-
Notifications
You must be signed in to change notification settings - Fork 12
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Idea: synchronous promise-like token #32
Comments
Here's a rough polyfill sketchexport class NotifyError extends Error {}
const currentNotifier = new AsyncContext.Variable()
function withNotifier(C) {
let notify
const token = new C(n => {
notify = n
})
if (typeof notify !== "function") {
throw new TypeError("Initializer must be called with a function.")
}
return {token, notify}
}
function resolveNotify(source, reason, notify) {
try {
if (typeof reason?.then === "function") {
let called = false
reason.then(inner => {
if (called) return
called = true
resolveNotify(source, inner, notify)
})
return
}
} catch (e) {
reason = e
}
if (reason === undefined) reason = source
try {
notify(reason)
} catch (e) {
reportError(e)
}
}
function performThen(onNotify, notify, reason) {
try {
reason = onNotify(reason)
} catch (e) {
reason = e
}
resolveNotify(source, reason, notify)
}
export class NotifyToken {
#reason = undefined
#listeners = new Set()
static #check(token) {
if (!(#listeners in token)) {
throw new TypeError("this must be a NotifyToken object")
}
}
constructor(init) {
init((reason = new NotifyError()) => {
const listeners = this.#listeners
if (listeners === undefined) return
this.#listeners = undefined
this.#reason = reason
for (const {notify, onNotify, remove, ref} of listeners) {
ref.#listeners?.delete(remove)
performThen(onNotify, reason, notify)
}
})
}
static all(tokens) {
const {token, notify} = withNotifier(this)
let initializing = true
let expected = 0
let reasons = new Set()
const source = new NotifyError()
const tryNotify = () => {
const list = reasons
if (list !== undefined && list.length >= expected) {
reasons = undefined
notify(new AggregateError(list, "", {cause: source}))
}
}
try {
for (const token of tokens) {
let called = false
expected++
resolveNotify(source, token, reason => {
if (called || reasons === undefined) return
called = true
reasons.add(token)
if (!initializing) tryNotify()
})
}
} catch (e) {
reasons.add(e === undefined ? source : e)
expected = reasons.length
}
initializing = false
tryNotify()
return token
}
static race(tokens) {
const {token, notify} = withNotifier(this)
let isEmpty = true
let reason
try {
for (const token of tokens) {
isEmpty = false
resolveNotify(token, notify)
}
} catch (e) {
reason = e
isEmpty = true
}
if (isEmpty) notify()
return token
}
static resolve(reason) {
const {token, notify} = withNotifier(this)
notify(reason)
return token
}
static withNotifier() {
const {token, notify} = withNotifier(this)
return {
token,
notify,
[Symbol.dispose]: notify,
}
}
static get current() {
NotifyToken.#check(this)
return currentNotifier.get()
}
get notified() {
NotifyToken.#check(this)
return this.#listeners === undefined
}
get reason() {
NotifyToken.#check(this)
return this.#reason
}
then(onNotify) {
NotifyToken.#check(this)
const {token, notify} = withNotifier(this.constructor)
const listeners = this.#listeners
if (listeners !== undefined) {
const current = currentNotifier.get()
if (current !== undefined) {
if (current.#listeners !== undefined) {
const listener = {onNotify, notify, remove: undefined, ref: current}
const remove = () => {
this.listeners?.delete(listener)
}
const other = {onNotify: remove, notify, remove: undefined, ref: current}
listener.other = other
listeners.push(listener)
current.#listeners.push(other)
return token
}
}
}
performThen(onNotify, this.#reason, notify)
return token
}
run(body) {
NotifyToken.#check(this)
return currentNotifier.run(this, body)
}
} |
I could imagine some common idioms: // From timeout
const token = new NotifyToken(notify => setTimeout(notify, 1000))
// From promise resolve
const token = new NotifyToken(notify => promise.then(notify))
// From promise reject (more common)
const token = new NotifyToken(notify => promise.catch(notify)) You could use atomics to avoid the DOM dependency for timeouts: const scheduler = Promise.resolve()
function timeout(ms, reason) {
return new NotifyToken(notify => {
const buf = new Int32Array(new SharedArrayBuffer(4))
const {async, value} = Atomics.waitAsync(buf, 0, 0, ms)
// Waiter set up, safe to signal
const current = NotifyToken.current
if (current !== undefined) {
try {
current.then(() => {
Atomics.notify(buf, 0, 1)
})
} catch (e) {
notify(e)
return
}
}
if (async) {
value.then(result => {
if (result !== "ok") notify(reason)
})
} else {
if (result !== "ok") notify(reason)
}
})
} |
Closed because #22's a better path. |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
I have an idea for how this could be done:
NotifyToken
, working similarly to promises. This token could also be used as a general composable one-shot can't-fail sync notifier, not just as a cancellation token..then
callback allows subscription and use inasync
/await
. You can also chain notify tokens and compose them with promises this way.@@dispose
property of the object returned fromNotifyToken.withNotifier()
exists so you can dousing {token} = NotifyToken.withNotifier()
instead of having to do a longtry { ... } finally { notify() }
block.NotifyToken.current
andtoken.run(() => ...)
are based on async context.token.notify
andtoken.reason
are mostly taken from the DOM standard (and I find myself usingabortSignal.aborted
a lot).NotifyToken.all
, the result is anAggregateError
of the inner reasons, and the reasons are deduplicated via SameValueZero.The key differences from promises is that 1. notification is synchronous and 2. return values and exceptions share a channel. The second makes this not "just" a synchronous
Promise
.The text was updated successfully, but these errors were encountered: