Files

190 lines
10 KiB
JavaScript

import dc, { Channel, channel, tracingChannel } from "node:diagnostics_channel";
import { AsyncLocalStorage } from "node:async_hooks";
const show = (label, ...values) => console.log(label + " " + JSON.stringify(values));
const events = ["start", "end", "asyncStart", "asyncEnd", "error"];
const record = (trace, output) => {
const handlers = Object.fromEntries(events.map((event) => [event, (context, name) => {
output.push([event, context.tag, context.result, context.error?.message, name === trace[event].name]);
}]));
trace.subscribe(handlers);
return handlers;
};
const errorShape = (fn) => {
try { fn(); return ["did-not-throw"]; }
catch (error) { return [error.name, error.code, error.message]; }
};
export function report() {
const symbol = Symbol("private-channel");
const ch = channel(symbol);
show("identity", channel(symbol) === ch, channel(Symbol("private-channel")) !== ch,
ch instanceof Channel, ch.name === symbol, dc.Channel === Channel,
channel("__proto__") === channel("__proto__"), dc.hasSubscribers(42));
const seen = [];
const late = (data, name) => seen.push(["late", data, name === symbol]);
const removed = (data) => seen.push(["removed", data]);
const first = (data) => { seen.push(["first", data]); ch.unsubscribe(removed); ch.subscribe(late); };
ch.subscribe(first);
ch.subscribe(removed);
ch.publish(1);
ch.publish(2);
show("publish-snapshot", seen, ch.hasSubscribers, dc.hasSubscribers(symbol));
show("unsubscribe", ch.unsubscribe(first), ch.unsubscribe(first), ch.unsubscribe(late),
ch.hasSubscribers, ch.unsubscribe(late), ch.hasSubscribers);
const duplicate = channel("duplicate");
const repeated = () => seen.push("duplicate");
duplicate.subscribe(repeated);
duplicate.subscribe(repeated);
show("duplicate", duplicate.unsubscribe(repeated), duplicate.hasSubscribers,
duplicate.unsubscribe(repeated), duplicate.hasSubscribers, duplicate.unsubscribe(repeated));
const trace = tracingChannel("sync-fixture");
show("trace-shape", trace.constructor.name, events.map((event) => trace[event].name),
events.every((event) => trace[event] === channel("tracing:sync-fixture:" + event)),
events.map((event) => {
const descriptor = Object.getOwnPropertyDescriptor(trace, event);
return [descriptor.writable, descriptor.enumerable, descriptor.configurable, descriptor.value === trace[event]];
}), trace.hasSubscribers);
const output = [];
const handlers = record(trace, output);
const context = { tag: "success" };
const receiver = { base: 10 };
const result = trace.traceSync(function (a, b) { return this.base + a + b; }, context, receiver, 2, 3);
show("sync", result, context.result === result, output.splice(0));
const objectResult = { value: "same object" };
const identityContext = { tag: "identity" };
show("sync-identity", trace.traceSync(() => objectResult, identityContext) === objectResult,
identityContext.result === objectResult, output.splice(0));
const boom = new Error("sync failure");
const failed = { tag: "failure" };
try { trace.traceSync(() => { throw boom; }, failed); }
catch (error) { show("sync-error", error === boom, failed.error === boom, output.splice(0)); }
trace.traceSync(() => "default context");
show("default-context", output.splice(0));
show("trace-unsubscribe", trace.unsubscribe(handlers), trace.unsubscribe(handlers),
trace.unsubscribe({}), trace.hasSubscribers);
const silent = {};
const promised = Promise.resolve(5);
show("silent", trace.traceSync(() => 8, silent), Object.keys(silent),
trace.tracePromise(() => promised, silent) === promised,
trace.traceCallback((value) => value, 0, silent, null, "not a callback"));
const explicit = tracingChannel(Object.fromEntries(events.map((event) => [event, channel("explicit:" + event)])));
const cloned = tracingChannel(explicit);
show("explicit", events.every((event) => explicit[event] === cloned[event]), explicit.hasSubscribers);
for (const value of [undefined, null, 42, false, {}, Symbol("bad")]) {
show("channel-error", errorShape(() => channel(value)));
}
for (const value of [undefined, null, 42, false, Symbol("bad")]) {
show("tracing-error", errorShape(() => tracingChannel(value)));
}
show("collection-error", errorShape(() => tracingChannel({ start: {} })));
const invalid = channel("invalid-subscription");
show("subscription-error", errorShape(() => invalid.subscribe(42)), invalid.hasSubscribers);
explicit.subscribe({ start: () => {} });
show("callback-error", errorShape(() => explicit.traceCallback(() => {}, 0, {}, null, "bad")));
const bound = channel("bound-stores");
const direct = new AsyncLocalStorage();
const shaped = new AsyncLocalStorage();
const transforms = [];
bound.bindStore(direct);
bound.bindStore(shaped, (data) => { transforms.push([data.value, direct.getStore()?.value ?? null]); return { wrapped: data.value }; });
show("bound-active", bound.hasSubscribers, dc.hasSubscribers(bound.name));
bound.subscribe((data) => show("stored-subscriber", direct.getStore() === data, shaped.getStore()));
const data = { value: "outer" };
const storedResult = bound.runStores(data, function (a, b) {
show("stores", direct.getStore() === data, shaped.getStore(), this.name, a, b);
bound.runStores({ value: "inner" }, () => show("nested-store", shaped.getStore()));
show("restored-store", direct.getStore() === data, shaped.getStore());
return "stored result";
}, { name: "receiver" }, 7, 8);
show("stores-result", storedResult, direct.getStore(), shaped.getStore(), transforms);
bound.bindStore(shaped, (data) => data.value);
bound.runStores(data, () => show("rebound", shaped.getStore()));
const storeError = new Error("store body failed");
try { bound.runStores(data, () => { throw storeError; }); }
catch (error) { show("store-error", error === storeError, direct.getStore(), shaped.getStore()); }
show("unbind", bound.unbindStore(direct), bound.unbindStore(direct), bound.unbindStore(shaped), bound.hasSubscribers);
const storesOnly = tracingChannel("stores-only");
storesOnly.start.bindStore(direct);
const storeContext = { tag: "only store" };
show("stores-only", storesOnly.hasSubscribers, storesOnly.traceSync(() => direct.getStore() === storeContext, storeContext),
direct.getStore(), storesOnly.start.unbindStore(direct), storesOnly.hasSubscribers);
}
export async function reportAsync() {
const trace = tracingChannel("async-fixture");
const output = [];
const late = trace.tracePromise(() => new Promise((resolve) => setImmediate(resolve, "silent")), { tag: "silent" });
record(trace, output);
await late;
show("late-subscription", output.splice(0));
const context = { tag: "promise" };
const receiver = { prefix: "bound:" };
const result = trace.tracePromise(function (value) { return Promise.resolve(this.prefix + value); }, context, receiver, "value");
show("promise-before", output.splice(0), context.result);
show("promise-after", await result, context.result, output.splice(0));
const objectResult = { value: "same object" };
const identityContext = { tag: "identity" };
const identityResult = await trace.tracePromise(() => Promise.resolve(objectResult), identityContext);
show("promise-identity", identityResult === objectResult, identityContext.result === objectResult, output.splice(0));
for (const value of [42, { then(resolve) { resolve("thenable"); } }]) {
const ctx = { tag: "coerced" };
show("coerced", await trace.tracePromise(() => value, ctx), ctx.result, output.splice(0));
}
const rejection = new Error("promise rejection");
const rejectedContext = { tag: "rejected" };
try { await trace.tracePromise(() => Promise.reject(rejection), rejectedContext); }
catch (error) { show("promise-error", error === rejection, rejectedContext.error === rejection, output.splice(0)); }
const thrown = new Error("promise sync throw");
try { trace.tracePromise(() => { throw thrown; }, { tag: "sync throw" }); }
catch (error) { show("promise-sync-error", error === thrown, output.splice(0)); }
const store = new AsyncLocalStorage();
trace.start.bindStore(store, (data) => { data.span = { name: data.tag }; return data.span; });
trace.asyncStart.bindStore(store, (data) => data.span);
const callbackContext = { tag: "callback" };
await new Promise((resolve) => {
const callbackReceiver = { who: "callback receiver" };
const returned = trace.traceCallback(function (value, callback) {
show("callback-body", this === receiver, store.getStore() === callbackContext.span);
setImmediate(() => callback.call(callbackReceiver, null, value, "extra"));
return "call result";
}, -1, callbackContext, receiver, "callback value", function (error, value, extra) {
show("callback", error, value, extra, this === callbackReceiver,
store.getStore() === callbackContext.span, output.splice(0));
resolve();
return "ignored callback return";
});
show("callback-return", returned, output.splice(0), store.getStore());
});
show("callback-end", output.splice(0), store.getStore());
const callbackError = new Error("callback failure");
const failedContext = { tag: "callback error" };
trace.traceCallback((callback) => callback(callbackError, "ignored result"), 0, failedContext, null,
(error) => show("callback-error-body", error === callbackError, failedContext.error === error, store.getStore() === failedContext.span));
show("callback-error", failedContext.result, output.splice(0), store.getStore());
const callbackThrow = new Error("callback threw");
try { trace.traceCallback((callback) => callback(null, "success"), 0, { tag: "callback throw" }, null, () => { throw callbackThrow; }); }
catch (error) { show("callback-throw", error === callbackThrow, output.splice(0)); }
}
export function subscriberThrow() {
const ch = channel("throwing-subscriber");
ch.subscribe(() => { console.log("throwing subscriber"); throw new Error("subscriber failed"); });
ch.subscribe(() => console.log("later subscriber"));
ch.publish({});
console.log("publish returned");
setImmediate(() => console.log("unreachable immediate"));
}
export function transformThrow() {
const ch = channel("throwing-transform");
const store = new AsyncLocalStorage();
ch.bindStore(store, () => { console.log("transform"); throw new Error("transform failed"); });
ch.subscribe(() => show("subscriber-store", store.getStore()));
show("transform-result", ch.runStores({}, () => "body continued"), store.getStore());
setImmediate(() => console.log("unreachable immediate"));
}