Initial Sample.

This commit is contained in:
2024-06-03 20:23:50 +05:30
parent ef2b65f673
commit 5269ec3c66
2575 changed files with 282312 additions and 0 deletions

View File

@@ -0,0 +1,4 @@
# @wry/context
Manage contextual information needed by synchronous or asynchronous
tasks without explicitly passing objects around.

View File

@@ -0,0 +1,235 @@
'use strict';
// This currentContext variable will only be used if the makeSlotClass
// function is called, which happens only if this is the first copy of the
// @wry/context package to be imported.
var currentContext = null;
// This unique internal object is used to denote the absence of a value
// for a given Slot, and is never exposed to outside code.
var MISSING_VALUE = {};
var idCounter = 1;
// Although we can't do anything about the cost of duplicated code from
// accidentally bundling multiple copies of the @wry/context package, we can
// avoid creating the Slot class more than once using makeSlotClass.
var makeSlotClass = function () { return /** @class */ (function () {
function Slot() {
// If you have a Slot object, you can find out its slot.id, but you cannot
// guess the slot.id of a Slot you don't have access to, thanks to the
// randomized suffix.
this.id = [
"slot",
idCounter++,
Date.now(),
Math.random().toString(36).slice(2),
].join(":");
}
Slot.prototype.hasValue = function () {
for (var context_1 = currentContext; context_1; context_1 = context_1.parent) {
// We use the Slot object iself as a key to its value, which means the
// value cannot be obtained without a reference to the Slot object.
if (this.id in context_1.slots) {
var value = context_1.slots[this.id];
if (value === MISSING_VALUE)
break;
if (context_1 !== currentContext) {
// Cache the value in currentContext.slots so the next lookup will
// be faster. This caching is safe because the tree of contexts and
// the values of the slots are logically immutable.
currentContext.slots[this.id] = value;
}
return true;
}
}
if (currentContext) {
// If a value was not found for this Slot, it's never going to be found
// no matter how many times we look it up, so we might as well cache
// the absence of the value, too.
currentContext.slots[this.id] = MISSING_VALUE;
}
return false;
};
Slot.prototype.getValue = function () {
if (this.hasValue()) {
return currentContext.slots[this.id];
}
};
Slot.prototype.withValue = function (value, callback,
// Given the prevalence of arrow functions, specifying arguments is likely
// to be much more common than specifying `this`, hence this ordering:
args, thisArg) {
var _a;
var slots = (_a = {
__proto__: null
},
_a[this.id] = value,
_a);
var parent = currentContext;
currentContext = { parent: parent, slots: slots };
try {
// Function.prototype.apply allows the arguments array argument to be
// omitted or undefined, so args! is fine here.
return callback.apply(thisArg, args);
}
finally {
currentContext = parent;
}
};
// Capture the current context and wrap a callback function so that it
// reestablishes the captured context when called.
Slot.bind = function (callback) {
var context = currentContext;
return function () {
var saved = currentContext;
try {
currentContext = context;
return callback.apply(this, arguments);
}
finally {
currentContext = saved;
}
};
};
// Immediately run a callback function without any captured context.
Slot.noContext = function (callback,
// Given the prevalence of arrow functions, specifying arguments is likely
// to be much more common than specifying `this`, hence this ordering:
args, thisArg) {
if (currentContext) {
var saved = currentContext;
try {
currentContext = null;
// Function.prototype.apply allows the arguments array argument to be
// omitted or undefined, so args! is fine here.
return callback.apply(thisArg, args);
}
finally {
currentContext = saved;
}
}
else {
return callback.apply(thisArg, args);
}
};
return Slot;
}()); };
function maybe(fn) {
try {
return fn();
}
catch (ignored) { }
}
// We store a single global implementation of the Slot class as a permanent
// non-enumerable property of the globalThis object. This obfuscation does
// nothing to prevent access to the Slot class, but at least it ensures the
// implementation (i.e. currentContext) cannot be tampered with, and all copies
// of the @wry/context package (hopefully just one) will share the same Slot
// implementation. Since the first copy of the @wry/context package to be
// imported wins, this technique imposes a steep cost for any future breaking
// changes to the Slot class.
var globalKey = "@wry/context:Slot";
var host =
// Prefer globalThis when available.
// https://github.com/benjamn/wryware/issues/347
maybe(function () { return globalThis; }) ||
// Fall back to global, which works in Node.js and may be converted by some
// bundlers to the appropriate identifier (window, self, ...) depending on the
// bundling target. https://github.com/endojs/endo/issues/576#issuecomment-1178515224
maybe(function () { return global; }) ||
// Otherwise, use a dummy host that's local to this module. We used to fall
// back to using the Array constructor as a namespace, but that was flagged in
// https://github.com/benjamn/wryware/issues/347, and can be avoided.
Object.create(null);
// Whichever globalHost we're using, make TypeScript happy about the additional
// globalKey property.
var globalHost = host;
var Slot = globalHost[globalKey] ||
// Earlier versions of this package stored the globalKey property on the Array
// constructor, so we check there as well, to prevent Slot class duplication.
Array[globalKey] ||
(function (Slot) {
try {
Object.defineProperty(globalHost, globalKey, {
value: Slot,
enumerable: false,
writable: false,
// When it was possible for globalHost to be the Array constructor (a
// legacy Slot dedup strategy), it was important for the property to be
// configurable:true so it could be deleted. That does not seem to be as
// important when globalHost is the global object, but I don't want to
// cause similar problems again, and configurable:true seems safest.
// https://github.com/endojs/endo/issues/576#issuecomment-1178274008
configurable: true
});
}
finally {
return Slot;
}
})(makeSlotClass());
var bind = Slot.bind, noContext = Slot.noContext;
function setTimeoutWithContext(callback, delay) {
return setTimeout(bind(callback), delay);
}
// Turn any generator function into an async function (using yield instead
// of await), with context automatically preserved across yields.
function asyncFromGen(genFn) {
return function () {
var gen = genFn.apply(this, arguments);
var boundNext = bind(gen.next);
var boundThrow = bind(gen.throw);
return new Promise(function (resolve, reject) {
function invoke(method, argument) {
try {
var result = method.call(gen, argument);
}
catch (error) {
return reject(error);
}
var next = result.done ? resolve : invokeNext;
if (isPromiseLike(result.value)) {
result.value.then(next, result.done ? reject : invokeThrow);
}
else {
next(result.value);
}
}
var invokeNext = function (value) { return invoke(boundNext, value); };
var invokeThrow = function (error) { return invoke(boundThrow, error); };
invokeNext();
});
};
}
function isPromiseLike(value) {
return value && typeof value.then === "function";
}
// If you use the fibers npm package to implement coroutines in Node.js,
// you should call this function at least once to ensure context management
// remains coherent across any yields.
var wrappedFibers = [];
function wrapYieldingFiberMethods(Fiber) {
// There can be only one implementation of Fiber per process, so this array
// should never grow longer than one element.
if (wrappedFibers.indexOf(Fiber) < 0) {
var wrap = function (obj, method) {
var fn = obj[method];
obj[method] = function () {
return noContext(fn, arguments, this);
};
};
// These methods can yield, according to
// https://github.com/laverdet/node-fibers/blob/ddebed9b8ae3883e57f822e2108e6943e5c8d2a8/fibers.js#L97-L100
wrap(Fiber, "yield");
wrap(Fiber.prototype, "run");
wrap(Fiber.prototype, "throwInto");
wrappedFibers.push(Fiber);
}
return Fiber;
}
exports.Slot = Slot;
exports.asyncFromGen = asyncFromGen;
exports.bind = bind;
exports.noContext = noContext;
exports.setTimeout = setTimeoutWithContext;
exports.wrapYieldingFiberMethods = wrapYieldingFiberMethods;
//# sourceMappingURL=bundle.cjs.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,235 @@
'use strict';
// This currentContext variable will only be used if the makeSlotClass
// function is called, which happens only if this is the first copy of the
// @wry/context package to be imported.
var currentContext = null;
// This unique internal object is used to denote the absence of a value
// for a given Slot, and is never exposed to outside code.
var MISSING_VALUE = {};
var idCounter = 1;
// Although we can't do anything about the cost of duplicated code from
// accidentally bundling multiple copies of the @wry/context package, we can
// avoid creating the Slot class more than once using makeSlotClass.
var makeSlotClass = function () { return /** @class */ (function () {
function Slot() {
// If you have a Slot object, you can find out its slot.id, but you cannot
// guess the slot.id of a Slot you don't have access to, thanks to the
// randomized suffix.
this.id = [
"slot",
idCounter++,
Date.now(),
Math.random().toString(36).slice(2),
].join(":");
}
Slot.prototype.hasValue = function () {
for (var context_1 = currentContext; context_1; context_1 = context_1.parent) {
// We use the Slot object iself as a key to its value, which means the
// value cannot be obtained without a reference to the Slot object.
if (this.id in context_1.slots) {
var value = context_1.slots[this.id];
if (value === MISSING_VALUE)
break;
if (context_1 !== currentContext) {
// Cache the value in currentContext.slots so the next lookup will
// be faster. This caching is safe because the tree of contexts and
// the values of the slots are logically immutable.
currentContext.slots[this.id] = value;
}
return true;
}
}
if (currentContext) {
// If a value was not found for this Slot, it's never going to be found
// no matter how many times we look it up, so we might as well cache
// the absence of the value, too.
currentContext.slots[this.id] = MISSING_VALUE;
}
return false;
};
Slot.prototype.getValue = function () {
if (this.hasValue()) {
return currentContext.slots[this.id];
}
};
Slot.prototype.withValue = function (value, callback,
// Given the prevalence of arrow functions, specifying arguments is likely
// to be much more common than specifying `this`, hence this ordering:
args, thisArg) {
var _a;
var slots = (_a = {
__proto__: null
},
_a[this.id] = value,
_a);
var parent = currentContext;
currentContext = { parent: parent, slots: slots };
try {
// Function.prototype.apply allows the arguments array argument to be
// omitted or undefined, so args! is fine here.
return callback.apply(thisArg, args);
}
finally {
currentContext = parent;
}
};
// Capture the current context and wrap a callback function so that it
// reestablishes the captured context when called.
Slot.bind = function (callback) {
var context = currentContext;
return function () {
var saved = currentContext;
try {
currentContext = context;
return callback.apply(this, arguments);
}
finally {
currentContext = saved;
}
};
};
// Immediately run a callback function without any captured context.
Slot.noContext = function (callback,
// Given the prevalence of arrow functions, specifying arguments is likely
// to be much more common than specifying `this`, hence this ordering:
args, thisArg) {
if (currentContext) {
var saved = currentContext;
try {
currentContext = null;
// Function.prototype.apply allows the arguments array argument to be
// omitted or undefined, so args! is fine here.
return callback.apply(thisArg, args);
}
finally {
currentContext = saved;
}
}
else {
return callback.apply(thisArg, args);
}
};
return Slot;
}()); };
function maybe(fn) {
try {
return fn();
}
catch (ignored) { }
}
// We store a single global implementation of the Slot class as a permanent
// non-enumerable property of the globalThis object. This obfuscation does
// nothing to prevent access to the Slot class, but at least it ensures the
// implementation (i.e. currentContext) cannot be tampered with, and all copies
// of the @wry/context package (hopefully just one) will share the same Slot
// implementation. Since the first copy of the @wry/context package to be
// imported wins, this technique imposes a steep cost for any future breaking
// changes to the Slot class.
var globalKey = "@wry/context:Slot";
var host =
// Prefer globalThis when available.
// https://github.com/benjamn/wryware/issues/347
maybe(function () { return globalThis; }) ||
// Fall back to global, which works in Node.js and may be converted by some
// bundlers to the appropriate identifier (window, self, ...) depending on the
// bundling target. https://github.com/endojs/endo/issues/576#issuecomment-1178515224
maybe(function () { return global; }) ||
// Otherwise, use a dummy host that's local to this module. We used to fall
// back to using the Array constructor as a namespace, but that was flagged in
// https://github.com/benjamn/wryware/issues/347, and can be avoided.
Object.create(null);
// Whichever globalHost we're using, make TypeScript happy about the additional
// globalKey property.
var globalHost = host;
var Slot = globalHost[globalKey] ||
// Earlier versions of this package stored the globalKey property on the Array
// constructor, so we check there as well, to prevent Slot class duplication.
Array[globalKey] ||
(function (Slot) {
try {
Object.defineProperty(globalHost, globalKey, {
value: Slot,
enumerable: false,
writable: false,
// When it was possible for globalHost to be the Array constructor (a
// legacy Slot dedup strategy), it was important for the property to be
// configurable:true so it could be deleted. That does not seem to be as
// important when globalHost is the global object, but I don't want to
// cause similar problems again, and configurable:true seems safest.
// https://github.com/endojs/endo/issues/576#issuecomment-1178274008
configurable: true
});
}
finally {
return Slot;
}
})(makeSlotClass());
var bind = Slot.bind, noContext = Slot.noContext;
function setTimeoutWithContext(callback, delay) {
return setTimeout(bind(callback), delay);
}
// Turn any generator function into an async function (using yield instead
// of await), with context automatically preserved across yields.
function asyncFromGen(genFn) {
return function () {
var gen = genFn.apply(this, arguments);
var boundNext = bind(gen.next);
var boundThrow = bind(gen.throw);
return new Promise(function (resolve, reject) {
function invoke(method, argument) {
try {
var result = method.call(gen, argument);
}
catch (error) {
return reject(error);
}
var next = result.done ? resolve : invokeNext;
if (isPromiseLike(result.value)) {
result.value.then(next, result.done ? reject : invokeThrow);
}
else {
next(result.value);
}
}
var invokeNext = function (value) { return invoke(boundNext, value); };
var invokeThrow = function (error) { return invoke(boundThrow, error); };
invokeNext();
});
};
}
function isPromiseLike(value) {
return value && typeof value.then === "function";
}
// If you use the fibers npm package to implement coroutines in Node.js,
// you should call this function at least once to ensure context management
// remains coherent across any yields.
var wrappedFibers = [];
function wrapYieldingFiberMethods(Fiber) {
// There can be only one implementation of Fiber per process, so this array
// should never grow longer than one element.
if (wrappedFibers.indexOf(Fiber) < 0) {
var wrap = function (obj, method) {
var fn = obj[method];
obj[method] = function () {
return noContext(fn, arguments, this);
};
};
// These methods can yield, according to
// https://github.com/laverdet/node-fibers/blob/ddebed9b8ae3883e57f822e2108e6943e5c8d2a8/fibers.js#L97-L100
wrap(Fiber, "yield");
wrap(Fiber.prototype, "run");
wrap(Fiber.prototype, "throwInto");
wrappedFibers.push(Fiber);
}
return Fiber;
}
exports.Slot = Slot;
exports.asyncFromGen = asyncFromGen;
exports.bind = bind;
exports.noContext = noContext;
exports.setTimeout = setTimeoutWithContext;
exports.wrapYieldingFiberMethods = wrapYieldingFiberMethods;
//# sourceMappingURL=bundle.cjs.map

View File

@@ -0,0 +1,7 @@
import { Slot } from "./slot.js";
export { Slot };
export declare const bind: <TArgs extends any[], TResult, TThis = any>(callback: (this: TThis, ...args: TArgs) => TResult) => (this: TThis, ...args: TArgs) => TResult, noContext: <TResult, TArgs extends any[], TThis = any>(callback: (this: TThis, ...args: TArgs) => TResult, args?: TArgs | undefined, thisArg?: TThis | undefined) => TResult;
export { setTimeoutWithContext as setTimeout };
declare function setTimeoutWithContext(callback: () => any, delay: number): any;
export declare function asyncFromGen<TArgs extends any[], TYield = any, TReturn = any, TNext = any>(genFn: (...args: TArgs) => Generator<TYield, TReturn, TNext>): (...args: TArgs) => Promise<any>;
export declare function wrapYieldingFiberMethods<F extends Function>(Fiber: F): F;

View File

@@ -0,0 +1,64 @@
import { Slot } from "./slot.js";
export { Slot };
export const { bind, noContext } = Slot;
// Like global.setTimeout, except the callback runs with captured context.
export { setTimeoutWithContext as setTimeout };
function setTimeoutWithContext(callback, delay) {
return setTimeout(bind(callback), delay);
}
// Turn any generator function into an async function (using yield instead
// of await), with context automatically preserved across yields.
export function asyncFromGen(genFn) {
return function () {
const gen = genFn.apply(this, arguments);
const boundNext = bind(gen.next);
const boundThrow = bind(gen.throw);
return new Promise((resolve, reject) => {
function invoke(method, argument) {
try {
var result = method.call(gen, argument);
}
catch (error) {
return reject(error);
}
const next = result.done ? resolve : invokeNext;
if (isPromiseLike(result.value)) {
result.value.then(next, result.done ? reject : invokeThrow);
}
else {
next(result.value);
}
}
const invokeNext = (value) => invoke(boundNext, value);
const invokeThrow = (error) => invoke(boundThrow, error);
invokeNext();
});
};
}
function isPromiseLike(value) {
return value && typeof value.then === "function";
}
// If you use the fibers npm package to implement coroutines in Node.js,
// you should call this function at least once to ensure context management
// remains coherent across any yields.
const wrappedFibers = [];
export function wrapYieldingFiberMethods(Fiber) {
// There can be only one implementation of Fiber per process, so this array
// should never grow longer than one element.
if (wrappedFibers.indexOf(Fiber) < 0) {
const wrap = (obj, method) => {
const fn = obj[method];
obj[method] = function () {
return noContext(fn, arguments, this);
};
};
// These methods can yield, according to
// https://github.com/laverdet/node-fibers/blob/ddebed9b8ae3883e57f822e2108e6943e5c8d2a8/fibers.js#L97-L100
wrap(Fiber, "yield");
wrap(Fiber.prototype, "run");
wrap(Fiber.prototype, "throwInto");
wrappedFibers.push(Fiber);
}
return Fiber;
}
//# sourceMappingURL=index.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}

View File

@@ -0,0 +1,12 @@
declare const makeSlotClass: () => {
new <TValue>(): {
readonly id: string;
hasValue(): boolean;
getValue(): TValue | undefined;
withValue<TResult, TArgs extends any[], TThis = any>(value: TValue, callback: (this: TThis, ...args: TArgs) => TResult, args?: TArgs | undefined, thisArg?: TThis | undefined): TResult;
};
bind<TArgs_1 extends any[], TResult_1, TThis_1 = any>(callback: (this: TThis_1, ...args: TArgs_1) => TResult_1): (this: TThis_1, ...args: TArgs_1) => TResult_1;
noContext<TResult_2, TArgs_2 extends any[], TThis_2 = any>(callback: (this: TThis_2, ...args: TArgs_2) => TResult_2, args?: TArgs_2 | undefined, thisArg?: TThis_2 | undefined): TResult_2;
};
export declare const Slot: ReturnType<typeof makeSlotClass>;
export {};

View File

@@ -0,0 +1,163 @@
// This currentContext variable will only be used if the makeSlotClass
// function is called, which happens only if this is the first copy of the
// @wry/context package to be imported.
let currentContext = null;
// This unique internal object is used to denote the absence of a value
// for a given Slot, and is never exposed to outside code.
const MISSING_VALUE = {};
let idCounter = 1;
// Although we can't do anything about the cost of duplicated code from
// accidentally bundling multiple copies of the @wry/context package, we can
// avoid creating the Slot class more than once using makeSlotClass.
const makeSlotClass = () => class Slot {
constructor() {
// If you have a Slot object, you can find out its slot.id, but you cannot
// guess the slot.id of a Slot you don't have access to, thanks to the
// randomized suffix.
this.id = [
"slot",
idCounter++,
Date.now(),
Math.random().toString(36).slice(2),
].join(":");
}
hasValue() {
for (let context = currentContext; context; context = context.parent) {
// We use the Slot object iself as a key to its value, which means the
// value cannot be obtained without a reference to the Slot object.
if (this.id in context.slots) {
const value = context.slots[this.id];
if (value === MISSING_VALUE)
break;
if (context !== currentContext) {
// Cache the value in currentContext.slots so the next lookup will
// be faster. This caching is safe because the tree of contexts and
// the values of the slots are logically immutable.
currentContext.slots[this.id] = value;
}
return true;
}
}
if (currentContext) {
// If a value was not found for this Slot, it's never going to be found
// no matter how many times we look it up, so we might as well cache
// the absence of the value, too.
currentContext.slots[this.id] = MISSING_VALUE;
}
return false;
}
getValue() {
if (this.hasValue()) {
return currentContext.slots[this.id];
}
}
withValue(value, callback,
// Given the prevalence of arrow functions, specifying arguments is likely
// to be much more common than specifying `this`, hence this ordering:
args, thisArg) {
const slots = {
__proto__: null,
[this.id]: value,
};
const parent = currentContext;
currentContext = { parent, slots };
try {
// Function.prototype.apply allows the arguments array argument to be
// omitted or undefined, so args! is fine here.
return callback.apply(thisArg, args);
}
finally {
currentContext = parent;
}
}
// Capture the current context and wrap a callback function so that it
// reestablishes the captured context when called.
static bind(callback) {
const context = currentContext;
return function () {
const saved = currentContext;
try {
currentContext = context;
return callback.apply(this, arguments);
}
finally {
currentContext = saved;
}
};
}
// Immediately run a callback function without any captured context.
static noContext(callback,
// Given the prevalence of arrow functions, specifying arguments is likely
// to be much more common than specifying `this`, hence this ordering:
args, thisArg) {
if (currentContext) {
const saved = currentContext;
try {
currentContext = null;
// Function.prototype.apply allows the arguments array argument to be
// omitted or undefined, so args! is fine here.
return callback.apply(thisArg, args);
}
finally {
currentContext = saved;
}
}
else {
return callback.apply(thisArg, args);
}
}
};
function maybe(fn) {
try {
return fn();
}
catch (ignored) { }
}
// We store a single global implementation of the Slot class as a permanent
// non-enumerable property of the globalThis object. This obfuscation does
// nothing to prevent access to the Slot class, but at least it ensures the
// implementation (i.e. currentContext) cannot be tampered with, and all copies
// of the @wry/context package (hopefully just one) will share the same Slot
// implementation. Since the first copy of the @wry/context package to be
// imported wins, this technique imposes a steep cost for any future breaking
// changes to the Slot class.
const globalKey = "@wry/context:Slot";
const host =
// Prefer globalThis when available.
// https://github.com/benjamn/wryware/issues/347
maybe(() => globalThis) ||
// Fall back to global, which works in Node.js and may be converted by some
// bundlers to the appropriate identifier (window, self, ...) depending on the
// bundling target. https://github.com/endojs/endo/issues/576#issuecomment-1178515224
maybe(() => global) ||
// Otherwise, use a dummy host that's local to this module. We used to fall
// back to using the Array constructor as a namespace, but that was flagged in
// https://github.com/benjamn/wryware/issues/347, and can be avoided.
Object.create(null);
// Whichever globalHost we're using, make TypeScript happy about the additional
// globalKey property.
const globalHost = host;
export const Slot = globalHost[globalKey] ||
// Earlier versions of this package stored the globalKey property on the Array
// constructor, so we check there as well, to prevent Slot class duplication.
Array[globalKey] ||
(function (Slot) {
try {
Object.defineProperty(globalHost, globalKey, {
value: Slot,
enumerable: false,
writable: false,
// When it was possible for globalHost to be the Array constructor (a
// legacy Slot dedup strategy), it was important for the property to be
// configurable:true so it could be deleted. That does not seem to be as
// important when globalHost is the global object, but I don't want to
// cause similar problems again, and configurable:true seems safest.
// https://github.com/endojs/endo/issues/576#issuecomment-1178274008
configurable: true
});
}
finally {
return Slot;
}
})(makeSlotClass());
//# sourceMappingURL=slot.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"slot.js","sourceRoot":"","sources":["../src/slot.ts"],"names":[],"mappings":"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"}

View File

@@ -0,0 +1,39 @@
{
"name": "@wry/context",
"version": "0.7.4",
"author": "Ben Newman <ben@eloper.dev>",
"description": "Manage contextual information needed by (a)synchronous tasks without explicitly passing objects around",
"license": "MIT",
"type": "module",
"main": "lib/bundle.cjs",
"module": "lib/index.js",
"types": "lib/index.d.ts",
"keywords": [],
"homepage": "https://github.com/benjamn/wryware",
"repository": {
"type": "git",
"url": "git+https://github.com/benjamn/wryware.git"
},
"bugs": {
"url": "https://github.com/benjamn/wryware/issues"
},
"scripts": {
"build": "npm run clean:before && npm run tsc && npm run rollup && npm run clean:after",
"clean:before": "rimraf lib",
"tsc": "npm run tsc:es5 && npm run tsc:esm",
"tsc:es5": "tsc -p tsconfig.es5.json",
"tsc:esm": "tsc -p tsconfig.json",
"rollup": "rollup -c rollup.config.js",
"clean:after": "rimraf lib/es5",
"prepare": "npm run build",
"test:cjs": "../../shared/test.sh lib/tests/bundle.cjs",
"test:esm": "../../shared/test.sh lib/tests/bundle.js",
"test": "npm run test:esm && npm run test:cjs"
},
"dependencies": {
"tslib": "^2.3.0"
},
"engines": {
"node": ">=8"
}
}

View File

@@ -0,0 +1 @@
export { default } from "../../shared/rollup.config.js";

View File

@@ -0,0 +1,87 @@
import { Slot } from "./slot.js";
export { Slot }
export const { bind, noContext } = Slot;
// Relying on the @types/node declaration of global.setTimeout can make
// things tricky for dowstream projects (see PR #7).
declare function setTimeout(
callback: (...args: any[]) => any,
ms?: number,
...args: any[]
): any;
// Like global.setTimeout, except the callback runs with captured context.
export { setTimeoutWithContext as setTimeout };
function setTimeoutWithContext(callback: () => any, delay: number) {
return setTimeout(bind(callback), delay);
}
// Turn any generator function into an async function (using yield instead
// of await), with context automatically preserved across yields.
export function asyncFromGen<
TArgs extends any[],
TYield = any,
TReturn = any,
TNext = any,
>(
genFn: (...args: TArgs) => Generator<TYield, TReturn, TNext>
) {
return function (this: any) {
const gen = genFn.apply(this, arguments as any);
type Method = (
this: Generator<TYield, TReturn, TNext>,
arg: any,
) => IteratorResult<TYield, TReturn>;
const boundNext: Method = bind(gen.next);
const boundThrow: Method = bind(gen.throw!);
return new Promise((resolve, reject) => {
function invoke(method: Method, argument: any) {
try {
var result: any = method.call(gen, argument);
} catch (error) {
return reject(error);
}
const next = result.done ? resolve : invokeNext;
if (isPromiseLike(result.value)) {
result.value.then(next, result.done ? reject : invokeThrow);
} else {
next(result.value);
}
}
const invokeNext = (value?: any) => invoke(boundNext, value);
const invokeThrow = (error: any) => invoke(boundThrow, error);
invokeNext();
});
} as (...args: TArgs) => Promise<any>;
}
function isPromiseLike(value: any): value is PromiseLike<any> {
return value && typeof value.then === "function";
}
// If you use the fibers npm package to implement coroutines in Node.js,
// you should call this function at least once to ensure context management
// remains coherent across any yields.
const wrappedFibers: Function[] = [];
export function wrapYieldingFiberMethods<F extends Function>(Fiber: F): F {
// There can be only one implementation of Fiber per process, so this array
// should never grow longer than one element.
if (wrappedFibers.indexOf(Fiber) < 0) {
const wrap = (obj: any, method: string) => {
const fn = obj[method];
obj[method] = function () {
return noContext(fn, arguments as any, this);
};
}
// These methods can yield, according to
// https://github.com/laverdet/node-fibers/blob/ddebed9b8ae3883e57f822e2108e6943e5c8d2a8/fibers.js#L97-L100
wrap(Fiber, "yield");
wrap(Fiber.prototype, "run");
wrap(Fiber.prototype, "throwInto");
wrappedFibers.push(Fiber);
}
return Fiber;
}

View File

@@ -0,0 +1,183 @@
type Context = {
parent: Context | null;
slots: { [slotId: string]: any };
}
// This currentContext variable will only be used if the makeSlotClass
// function is called, which happens only if this is the first copy of the
// @wry/context package to be imported.
let currentContext: Context | null = null;
// This unique internal object is used to denote the absence of a value
// for a given Slot, and is never exposed to outside code.
const MISSING_VALUE: any = {};
let idCounter = 1;
// Although we can't do anything about the cost of duplicated code from
// accidentally bundling multiple copies of the @wry/context package, we can
// avoid creating the Slot class more than once using makeSlotClass.
const makeSlotClass = () => class Slot<TValue> {
// If you have a Slot object, you can find out its slot.id, but you cannot
// guess the slot.id of a Slot you don't have access to, thanks to the
// randomized suffix.
public readonly id = [
"slot",
idCounter++,
Date.now(),
Math.random().toString(36).slice(2),
].join(":");
public hasValue() {
for (let context = currentContext; context; context = context.parent) {
// We use the Slot object iself as a key to its value, which means the
// value cannot be obtained without a reference to the Slot object.
if (this.id in context.slots) {
const value = context.slots[this.id];
if (value === MISSING_VALUE) break;
if (context !== currentContext) {
// Cache the value in currentContext.slots so the next lookup will
// be faster. This caching is safe because the tree of contexts and
// the values of the slots are logically immutable.
currentContext!.slots[this.id] = value;
}
return true;
}
}
if (currentContext) {
// If a value was not found for this Slot, it's never going to be found
// no matter how many times we look it up, so we might as well cache
// the absence of the value, too.
currentContext.slots[this.id] = MISSING_VALUE;
}
return false;
}
public getValue(): TValue | undefined {
if (this.hasValue()) {
return currentContext!.slots[this.id] as TValue;
}
}
public withValue<TResult, TArgs extends any[], TThis = any>(
value: TValue,
callback: (this: TThis, ...args: TArgs) => TResult,
// Given the prevalence of arrow functions, specifying arguments is likely
// to be much more common than specifying `this`, hence this ordering:
args?: TArgs,
thisArg?: TThis,
): TResult {
const slots = {
__proto__: null,
[this.id]: value,
};
const parent = currentContext;
currentContext = { parent, slots };
try {
// Function.prototype.apply allows the arguments array argument to be
// omitted or undefined, so args! is fine here.
return callback.apply(thisArg!, args!);
} finally {
currentContext = parent;
}
}
// Capture the current context and wrap a callback function so that it
// reestablishes the captured context when called.
static bind<TArgs extends any[], TResult, TThis = any>(
callback: (this: TThis, ...args: TArgs) => TResult,
) {
const context = currentContext;
return function (this: TThis) {
const saved = currentContext;
try {
currentContext = context;
return callback.apply(this, arguments as any);
} finally {
currentContext = saved;
}
} as typeof callback;
}
// Immediately run a callback function without any captured context.
static noContext<TResult, TArgs extends any[], TThis = any>(
callback: (this: TThis, ...args: TArgs) => TResult,
// Given the prevalence of arrow functions, specifying arguments is likely
// to be much more common than specifying `this`, hence this ordering:
args?: TArgs,
thisArg?: TThis,
) {
if (currentContext) {
const saved = currentContext;
try {
currentContext = null;
// Function.prototype.apply allows the arguments array argument to be
// omitted or undefined, so args! is fine here.
return callback.apply(thisArg!, args!);
} finally {
currentContext = saved;
}
} else {
return callback.apply(thisArg!, args!);
}
}
};
function maybe<T>(fn: () => T): T | undefined {
try {
return fn();
} catch (ignored) {}
}
// We store a single global implementation of the Slot class as a permanent
// non-enumerable property of the globalThis object. This obfuscation does
// nothing to prevent access to the Slot class, but at least it ensures the
// implementation (i.e. currentContext) cannot be tampered with, and all copies
// of the @wry/context package (hopefully just one) will share the same Slot
// implementation. Since the first copy of the @wry/context package to be
// imported wins, this technique imposes a steep cost for any future breaking
// changes to the Slot class.
const globalKey = "@wry/context:Slot";
const host =
// Prefer globalThis when available.
// https://github.com/benjamn/wryware/issues/347
maybe(() => globalThis) ||
// Fall back to global, which works in Node.js and may be converted by some
// bundlers to the appropriate identifier (window, self, ...) depending on the
// bundling target. https://github.com/endojs/endo/issues/576#issuecomment-1178515224
maybe(() => global) ||
// Otherwise, use a dummy host that's local to this module. We used to fall
// back to using the Array constructor as a namespace, but that was flagged in
// https://github.com/benjamn/wryware/issues/347, and can be avoided.
Object.create(null) as typeof Array;
// Whichever globalHost we're using, make TypeScript happy about the additional
// globalKey property.
const globalHost: typeof host & {
[globalKey]?: typeof Slot;
} = host;
export const Slot: ReturnType<typeof makeSlotClass> =
globalHost[globalKey] ||
// Earlier versions of this package stored the globalKey property on the Array
// constructor, so we check there as well, to prevent Slot class duplication.
(Array as typeof globalHost)[globalKey] ||
(function (Slot) {
try {
Object.defineProperty(globalHost, globalKey, {
value: Slot,
enumerable: false,
writable: false,
// When it was possible for globalHost to be the Array constructor (a
// legacy Slot dedup strategy), it was important for the property to be
// configurable:true so it could be deleted. That does not seem to be as
// important when globalHost is the global object, but I don't want to
// cause similar problems again, and configurable:true seems safest.
// https://github.com/endojs/endo/issues/576#issuecomment-1178274008
configurable: true
});
} finally {
return Slot;
}
})(makeSlotClass());

View File

@@ -0,0 +1,478 @@
import * as assert from "assert";
import {
Slot,
bind,
noContext,
setTimeout,
asyncFromGen
} from "../index.js";
function repeat(s: string, times: number) {
let result = "";
while (times --> 0) result += s;
return result;
}
describe("Slot", function () {
it("is importable", function () {
assert.strictEqual(typeof Slot, "function");
});
it("has no value initially", function () {
const slot = new Slot;
assert.strictEqual(slot.hasValue(), false);
assert.strictEqual(typeof slot.getValue(), "undefined");
});
it("retains values set by withValue", function () {
const slot = new Slot<number>();
const results = slot.withValue(123, () => {
assert.strictEqual(slot.hasValue(), true);
assert.strictEqual(slot.getValue(), 123);
const results = [
slot.getValue(),
slot.withValue(456, () => {
assert.strictEqual(slot.hasValue(), true);
return slot.getValue();
}),
slot.withValue(789, () => {
assert.strictEqual(slot.hasValue(), true);
return slot.getValue();
}),
];
assert.strictEqual(slot.hasValue(), true);
assert.strictEqual(slot.getValue(), 123);
return results;
});
assert.strictEqual(slot.hasValue(), false);
assert.deepEqual(results, [123, 456, 789]);
});
it("is not confused by other slots", function () {
const stringSlot = new Slot<string>();
const numberSlot = new Slot<number>();
function inner() {
return repeat(
stringSlot.getValue()!,
numberSlot.getValue()!,
);
}
const oneWay = stringSlot.withValue("oyez", () => {
return numberSlot.withValue(3, inner);
});
assert.strictEqual(stringSlot.hasValue(), false);
assert.strictEqual(numberSlot.hasValue(), false);
const otherWay = numberSlot.withValue(3, () => {
return stringSlot.withValue("oyez", inner);
});
assert.strictEqual(stringSlot.hasValue(), false);
assert.strictEqual(numberSlot.hasValue(), false);
assert.strictEqual(oneWay, otherWay);
assert.strictEqual(oneWay, "oyezoyezoyez");
});
it("is a singleton", async function () {
const cjsSlotModule = await import("../slot.js");
assert.ok(new Slot<number>() instanceof cjsSlotModule.Slot);
assert.ok(new cjsSlotModule.Slot() instanceof Slot);
assert.strictEqual(cjsSlotModule.Slot, Slot);
const globalKey = "@wry/context:Slot";
assert.strictEqual((global as any)[globalKey], Slot);
assert.deepEqual(Object.keys(Array), []);
assert.strictEqual(
Object.keys(global).indexOf(globalKey),
-1,
);
});
it("can be subclassed", function () {
class NamedSlot extends Slot<number> {
constructor(public readonly name: string) {
super();
(this as any).id = name + ":" + this.id;
}
}
const ageSlot = new NamedSlot("age");
assert.strictEqual(ageSlot.hasValue(), false);
ageSlot.withValue(87, () => {
assert.strictEqual(ageSlot.hasValue(), true);
const age = ageSlot.getValue()!;
assert.strictEqual(age, 87);
assert.strictEqual(ageSlot.name, "age");
assert.ok(ageSlot.id.startsWith("age:slot:"));
});
class DefaultSlot<T> extends Slot<T> {
constructor(public readonly defaultValue: T) {
super();
}
hasValue() {
return true;
}
getValue() {
return super.hasValue() ? super.getValue() : this.defaultValue;
}
}
const defaultSlot = new DefaultSlot("default");
assert.strictEqual(defaultSlot.hasValue(), true);
assert.strictEqual(defaultSlot.getValue(), "default");
const check = defaultSlot.withValue("real", function () {
assert.strictEqual(defaultSlot.hasValue(), true);
assert.strictEqual(defaultSlot.getValue(), "real");
return bind(function () {
assert.strictEqual(defaultSlot.hasValue(), true);
assert.strictEqual(defaultSlot.getValue(), "real");
});
});
assert.strictEqual(defaultSlot.hasValue(), true);
assert.strictEqual(defaultSlot.getValue(), "default");
check();
});
});
describe("bind", function () {
it("is importable", function () {
assert.strictEqual(typeof bind, "function");
});
it("preserves multiple slots", function () {
const stringSlot = new Slot<string>();
const numberSlot = new Slot<number>();
function neither() {
assert.strictEqual(stringSlot.hasValue(), false);
assert.strictEqual(numberSlot.hasValue(), false);
}
const checks = [bind(neither)];
stringSlot.withValue("asdf", () => {
function justStringAsdf() {
assert.strictEqual(stringSlot.hasValue(), true);
assert.strictEqual(stringSlot.getValue(), "asdf");
assert.strictEqual(numberSlot.hasValue(), false);
}
checks.push(bind(justStringAsdf));
numberSlot.withValue(54321, () => {
checks.push(bind(function both() {
assert.strictEqual(stringSlot.hasValue(), true);
assert.strictEqual(stringSlot.getValue(), "asdf");
assert.strictEqual(numberSlot.hasValue(), true);
assert.strictEqual(numberSlot.getValue(), 54321);
}));
});
stringSlot.withValue("oyez", () => {
checks.push(bind(function justStringOyez() {
assert.strictEqual(stringSlot.hasValue(), true);
assert.strictEqual(stringSlot.getValue(), "oyez");
assert.strictEqual(numberSlot.hasValue(), false);
}));
numberSlot.withValue(12345, () => {
checks.push(bind(function bothAgain() {
assert.strictEqual(stringSlot.hasValue(), true);
assert.strictEqual(stringSlot.getValue(), "oyez");
assert.strictEqual(numberSlot.hasValue(), true);
assert.strictEqual(numberSlot.getValue(), 12345);
}));
});
});
checks.push(bind(justStringAsdf));
});
checks.push(bind(neither));
checks.forEach(check => check());
});
it("does not permit rebinding", function () {
const slot = new Slot<number>();
const bound = slot.withValue(1, () => bind(function () {
assert.strictEqual(slot.hasValue(), true);
assert.strictEqual(slot.getValue(), 1);
return slot.getValue();
}));
assert.strictEqual(bound(), 1);
const rebound = slot.withValue(2, () => bind(bound));
assert.strictEqual(rebound(), 1);
assert.strictEqual(slot.hasValue(), false);
});
});
describe("noContext", function () {
it("is importable", function () {
assert.strictEqual(typeof noContext, "function");
});
it("severs context set by withValue", function () {
const slot = new Slot<string>();
const result = slot.withValue("asdf", function () {
assert.strictEqual(slot.getValue(), "asdf");
return noContext(() => {
assert.strictEqual(slot.hasValue(), false);
return "inner";
});
});
assert.strictEqual(result, "inner");
});
it("severs bound context", function () {
const slot = new Slot<string>();
const bound = slot.withValue("asdf", function () {
assert.strictEqual(slot.getValue(), "asdf");
return bind(function () {
assert.strictEqual(slot.getValue(), "asdf");
return noContext(() => {
assert.strictEqual(slot.hasValue(), false);
return "inner";
});
});
});
assert.strictEqual(slot.hasValue(), false);
assert.strictEqual(bound(), "inner");
});
it("permits reestablishing inner context values", function () {
const slot = new Slot<string>();
const bound = slot.withValue("asdf", function () {
assert.strictEqual(slot.getValue(), "asdf");
return bind(function () {
assert.strictEqual(slot.getValue(), "asdf");
return noContext(() => {
assert.strictEqual(slot.hasValue(), false);
return slot.withValue("oyez", () => {
assert.strictEqual(slot.hasValue(), true);
return slot.getValue();
});
});
});
});
assert.strictEqual(slot.hasValue(), false);
assert.strictEqual(bound(), "oyez");
});
it("permits passing arguments and this", function () {
const slot = new Slot<number>();
const self = {};
const notSelf = {};
const result = slot.withValue(1, function (a: number) {
assert.strictEqual(slot.hasValue(), true);
assert.strictEqual(slot.getValue(), 1);
assert.strictEqual(this, self);
return noContext(function (b: number) {
assert.strictEqual(slot.hasValue(), false);
assert.strictEqual(this, notSelf);
return slot.withValue(b, (aArg, bArg) => {
assert.strictEqual(slot.hasValue(), true);
assert.strictEqual(slot.getValue(), b);
assert.strictEqual(this, notSelf);
assert.strictEqual(a, aArg);
assert.strictEqual(b, bArg);
return aArg * bArg;
}, [a, b], self);
}, [3], notSelf);
}, [2], self);
assert.strictEqual(result, 2 * 3);
});
it("works with Array-like (arguments) objects", function () {
function multiply(a: number, b: number) {
return noContext(function inner(a, b) {
return a * b;
}, arguments as any);
}
assert.strictEqual(multiply(3, 7) * 2, 42);
});
});
describe("setTimeout", function () {
it("is importable", function () {
assert.strictEqual(typeof setTimeout, "function");
});
it("binds its callback", function () {
const booleanSlot = new Slot<boolean>();
const objectSlot = new Slot<{ foo: number }>();
return new Promise<void>((resolve, reject) => {
booleanSlot.withValue(true, () => {
assert.strictEqual(booleanSlot.getValue(), true);
objectSlot.withValue({ foo: 42 }, () => {
setTimeout(function () {
try {
assert.strictEqual(booleanSlot.hasValue(), true);
assert.strictEqual(booleanSlot.getValue(), true);
assert.strictEqual(objectSlot.hasValue(), true);
assert.strictEqual(objectSlot.getValue()!.foo, 42);
resolve();
} catch (error) {
reject(error);
}
}, 10);
})
});
}).then(() => {
assert.strictEqual(booleanSlot.hasValue(), false);
assert.strictEqual(objectSlot.hasValue(), false);
});
});
});
describe("asyncFromGen", function () {
it("is importable", function () {
assert.strictEqual(typeof asyncFromGen, "function");
});
it("works like an async function", asyncFromGen(
function*(): Generator<number | Promise<number>, Promise<string>, number> {
let sum = 0;
const limit = yield new Promise(resolve => {
setTimeout(() => resolve(10), 10);
});
for (let i = 0; i < limit; ++i) {
sum += yield i + 1;
}
assert.strictEqual(sum, 55);
return Promise.resolve("ok");
},
));
it("properly handles exceptions", async function () {
const fn = asyncFromGen(function*(throwee?: object) {
const result = yield Promise.resolve("ok");
if (throwee) {
throw yield throwee;
}
return result;
});
const okPromise = fn();
const expected = {};
const koPromise = fn(expected);
assert.strictEqual(await okPromise, "ok");
try {
await koPromise;
throw new Error("not reached");
} catch (error) {
assert.strictEqual(error, expected);
}
try {
await fn(Promise.resolve("oyez"));
throw new Error("not reached");
} catch (thrown) {
assert.strictEqual(thrown, "oyez");
}
});
it("propagates contextual slot values across yields", function () {
const stringSlot = new Slot<string>();
const numberSlot = new Slot<number>();
function checkNoValues() {
assert.strictEqual(stringSlot.hasValue(), false);
assert.strictEqual(numberSlot.hasValue(), false);
}
const inner = asyncFromGen(function*(
stringValue: string,
numberValue: number,
) {
function checkValues() {
assert.strictEqual(stringSlot.getValue(), stringValue);
assert.strictEqual(numberSlot.getValue(), numberValue);
}
checkValues();
yield new Promise<void>(resolve => setTimeout(function () {
checkValues();
resolve();
}, 10));
checkValues();
yield new Promise<void>(resolve => {
checkValues();
resolve();
});
checkValues();
yield Promise.resolve().then(checkNoValues);
checkValues();
return repeat(stringValue, numberValue);
});
const outer = asyncFromGen(function*() {
checkNoValues();
const oyezPromise = stringSlot.withValue("oyez", () => {
return numberSlot.withValue(3, () => inner("oyez", 3));
});
checkNoValues();
const hahaPromise = numberSlot.withValue(4, () => {
return stringSlot.withValue("ha", () => inner("ha", 4));
});
checkNoValues();
assert.strictEqual(yield oyezPromise, "oyezoyezoyez");
assert.strictEqual(yield hahaPromise, "hahahaha");
checkNoValues();
return Promise.all([oyezPromise, hahaPromise]);
});
return outer().then(results => {
checkNoValues();
assert.deepEqual(results, [
"oyezoyezoyez",
"hahahaha",
]);
});
});
it("allows Promise rejections to be caught", function () {
const fn = asyncFromGen(function*() {
try {
yield Promise.reject(new Error("expected"));
throw new Error("not reached");
} catch (error: any) {
assert.strictEqual(error?.message, "expected");
}
return "ok";
});
return fn().then(result => {
assert.strictEqual(result, "ok");
});
});
});