Shared functions (and variables) could be the solution to the problem of methods, and if done correctly, could avoid most of the complexity of binding stuff to realms. They'd be heavily restricted, of course.
Shared functions would be declared via the shared keyword, much like async functions.
Shared variables would be declared like shared var, shared let, shared const, and shared using.
Shared functions inherit from SharedFunction.prototype, isolating its linkage fully from unshared data.
Shared functions, outside their body, can only access shared variables and properties of the global object.
The global object reference is that of the calling context's realm. This not only simplifies the calling sequence, but it also makes shared functions independently useful as a security primitive with realms.
Shared functions can be async, in which they use the calling context's realm to get the right Promise reference to construct and return.
Shared variables have the same read and write rules as shared struct members.
Shared structs' prototypes are themselves either null or shared structs. And yes, shared structs can extend other shared structs.
As per usual, SharedStruct.prototype.constructor === SharedStruct.
Shared struct constructors, static methods, and instance methods are implicitly shared. If you want to do things that can't be shared, it can't be in a shared struct definition.
Shared constants and read-only shared structs' properties do not require unsafe. Shared structs and their instance prototypes are themselves read-only, to improve ergonomics.
Built-in ES methods are only shared if explicitly stated as such.
To ease certain messaging use cases, registered struct Type and a registered "name" struct Type declaration exists to register a struct so that shared structs from other contexts can be matched to their local counterparts. A name can only be registered once, and a shared struct can only be registered once.
The name is configurable so minifiers and bundlers can still rename them and not either blow up bundle size or awkwardly dance around stuff.
Atomics.registeredKeyFor(Type) and Atomics.registeredTypeFor("Type") can inspect this registry.
Things shared variables and shared struct properties can be set to:
Shared functions
Shared arrays
Shared structs
Non-object values
When the shadow realms proposal comes around, those can also be added to this list, provided its methods employ the requisite locking to ensure it only runs on one calling agent at a time.
This should hopefully bring the flexibility needed. And with this API, I also propose the following modified API:
declare namespace Atomics {
// same as before, including other new extensions...
// New structs, all registered with internal names in reality
export registered internal struct UnlockToken {
constructor()
get locked(): boolean
unlock(): undefined
[Symbol.dispose](): undefined
}
export registered internal struct Mutex {
constructor()
lock(token?: UnlockToken): UnlockToken
lockIfAvailable(timeout: number, token?: UnlockToken): UnlockToken
}
export registered internal struct Condition {
constructor()
wait(token: UnlockToken): undefined
waitFor(token: UnlockToken, timeout: number, predicate?: () => boolean): undefined
notify(count?: number): undefined
}
}
An example counter object might look like this:
shared struct Counter {
value = 0
inc() {
let value = Atomics.load(this, "value")
while (true) {
let prev = Atomics.compareExchange(this, "value", value, value + 1)
if (prev === value) return value + 1
value = prev
}
}
}
StructRegistry.add(internalToken, Atomics.Mutex)
// Inside worker A:
shared struct SharedThing {
x;
y;
foo() {
// Dynamically scoped to the caller's global, thus it always "just works"
console.log("do a foo");
}
};
let thing1 = new SharedThing;
workerB.postMessage(thing1);
// Inside worker B:
onmessage = (thing) => {
// undefined is not a function, and `SharedThing` didn't even need to be defined!
thing.foo();
};
Shared functions (and variables) could be the solution to the problem of methods, and if done correctly, could avoid most of the complexity of binding stuff to realms. They'd be heavily restricted, of course.
shared
keyword, much likeasync
functions.shared var
,shared let
,shared const
, andshared using
.SharedFunction.prototype
, isolating its linkage fully from unshared data.async
, in which they use the calling context's realm to get the rightPromise
reference to construct and return.extend
other shared structs.SharedStruct.prototype.constructor === SharedStruct
.shared
. If you want to do things that can't beshared
, it can't be in ashared struct
definition.unsafe
. Shared structs and their instance prototypes are themselves read-only, to improve ergonomics.shared
if explicitly stated as such.registered struct Type
and aregistered "name" struct Type
declaration exists to register a struct so that shared structs from other contexts can be matched to their local counterparts. A name can only be registered once, and a shared struct can only be registered once.Atomics.registeredKeyFor(Type)
andAtomics.registeredTypeFor("Type")
can inspect this registry.Things shared variables and shared struct properties can be set to:
This should hopefully bring the flexibility needed. And with this API, I also propose the following modified API:
An example counter object might look like this:
The code from https://github.com/tc39/proposal-structs/blob/main/ATTACHING-BEHAVIOR.md#coordinating-identity-continuity-among-workers could look like this instead: