@matatbread/matbot-plugin-api 0.4.9 → 0.4.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "node",
5
5
  "browser"
6
6
  ],
7
- "version": "0.4.9",
7
+ "version": "0.4.11",
8
8
  "engines": {
9
9
  "node": ">=24.0.0"
10
10
  },
@@ -23,8 +23,9 @@ export interface PrincipalCarrier {
23
23
  /**
24
24
  * Establish `principal` for the dynamic extent of `fn` and return whatever `fn` returns. On node
25
25
  * this is per-async-flow (concurrent turns stay isolated); nesting cleanly shadows-and-restores,
26
- * which is how in-flow delegation works. Consume any async iterable *inside* `fn` — returning an
27
- * unconsumed iterator drops the scope before it is pulled.
26
+ * which is how in-flow delegation works. An implementation establishes the scope and nothing more:
27
+ * deferred work returned out of it (an unpulled iterator, a promise of one) is {@link runAs}'s to
28
+ * re-establish, and doing it here as well would only nest the same identity twice.
28
29
  */
29
30
  run<T>(principal: Principal, fn: () => T): T;
30
31
  /**
@@ -71,7 +72,75 @@ export function tryCurrentPrincipal(): Principal | undefined {
71
72
 
72
73
  /** Run `fn` with `principal` established as the ambient identity for its async extent. */
73
74
  export function runAs<T>(principal: Principal, fn: () => T): T {
74
- return need().run(principal, fn);
75
+ const carrier = need();
76
+ return carrier.run(principal, () => rescope(carrier, principal, fn()));
77
+ }
78
+
79
+ // A generator's body does not begin until the first pull, so an iterator returned out of the scope carries
80
+ // its whole extent with it: `fn` established the identity for the *construction*, and the work then runs in
81
+ // whatever flow happens to pull it. The tool ABI returns exactly that shape (`executor.execute()`), so the
82
+ // natural thing to write at a tool seam was the broken thing — silently, since a host that entered a boot
83
+ // principal reads a plausible one rather than throwing. Re-establishing per pull rather than requiring the
84
+ // caller to consume inside `fn`: the principal is a re-entrant label that grants nothing, and nesting
85
+ // already shadows-and-restores, so re-entering costs nothing semantically — which is also what makes nesting
86
+ // unremarkable: `runAs(A, () => runAs(B, () => gen()))` stacks two wrappers and every pull resolves to B,
87
+ // the same answer plain nesting gives with no iterator in sight. Deliberately NOT extended to
88
+ // `machineBusy` or `withUsageScope`, whose shape is identical but whose hold and roll-up settle when `fn`
89
+ // does — a resource with a settle edge cannot be re-entered per pull, only an identity can.
90
+ function rescope<T>(carrier: PrincipalCarrier, principal: Principal, value: T): T {
91
+ if (isAsyncIterator(value)) return scopedIterator(carrier, principal, value) as T;
92
+ // `async () => execute(...)` defers the identical body behind one await. This DERIVES a promise rather
93
+ // than intercepting one, which is why a single `then` covers it: `catch`/`finally` on what the caller
94
+ // receives are downstream of the rescope, and a rejection is a value rather than a deferred body, so an
95
+ // `onRejected` would have nothing to re-establish and would cost the rejection's identity. Native only —
96
+ // `PromiseLike.then` need not return a promise, so adopting a thenable would replace an object the caller
97
+ // may use for more than awaiting (a chainable query builder), and `async` produces the native one anyway.
98
+ if (value instanceof Promise) return value.then(inner => rescope(carrier, principal, inner)) as T;
99
+ return value;
100
+ }
101
+
102
+ // Iterator-shaped, not merely async-iterable. A ReadableStream is `for await`-able too, and CAN be proxied
103
+ // (measured: `instanceof`, `pipeTo` and a platform `new Response(stream)` all survive one) — but only two of
104
+ // its pull paths could then be re-entered, `[Symbol.asyncIterator]` and `getReader().read()`, while
105
+ // `pipeTo`/`pipeThrough`/`tee` pull from platform internals no wrapper reaches. A conditional guarantee is
106
+ // worse than none in an identity primitive: a host testing with `for await` and shipping `pipeTo` would
107
+ // regress silently. Its eager half is safe either way — `start()` runs at construction, inside the scope —
108
+ // so what is uncovered is a lazy `pull()` that reads the principal, which no in-repo streamer is: every one
109
+ // is an async generator, and `FileHandle.stream()` is `AsyncIterable` by contract. Carrying `next` is then
110
+ // taken to mean the value IS the iterator — what a caller holding `execute()`'s result has — rather than a
111
+ // factory for fresh ones, a thing it cannot also be.
112
+ function isAsyncIterator(value: unknown): value is AsyncIterator<unknown> & AsyncIterable<unknown> {
113
+ return typeof value === 'object' && value !== null
114
+ && typeof (value as AsyncIterable<unknown>)[Symbol.asyncIterator] === 'function'
115
+ && typeof (value as AsyncIterator<unknown>).next === 'function';
116
+ }
117
+
118
+ function scopedIterator<T>(
119
+ carrier: PrincipalCarrier,
120
+ principal: Principal,
121
+ source: AsyncIterator<T> & AsyncIterable<T>,
122
+ ): AsyncIterableIterator<T> {
123
+ const inScope = <R>(f: () => R): R => carrier.run(principal, f);
124
+ // A Proxy rather than a fresh object literal: the value crossing back out must still BE what it was —
125
+ // a custom iterator's own methods, its prototype, `instanceof` — so only the pull points are replaced.
126
+ // Anything else is forwarded bound to the target, which a class-based iterator holding internal state
127
+ // (or an object with internal slots) requires.
128
+ const pulls: Record<PropertyKey, unknown> = {
129
+ [Symbol.asyncIterator]: () => proxy,
130
+ next: (...args: [] | [undefined]) => inScope(() => source.next(...args)),
131
+ ...(source.return !== undefined ? { return: (value?: unknown) => inScope(() => source.return!(value)) } : {}),
132
+ ...(source.throw !== undefined ? { throw: (error?: unknown) => inScope(() => source.throw!(error)) } : {}),
133
+ };
134
+ const proxy = new Proxy(source, {
135
+ // `hasOwn`, not `in`: the latter reaches Object.prototype, which would serve `toString`/`constructor`
136
+ // off the lookup object instead of the iterator being wrapped.
137
+ get(target, prop) {
138
+ if (Object.hasOwn(pulls, prop)) return pulls[prop];
139
+ const value = Reflect.get(target, prop, target);
140
+ return typeof value === 'function' ? value.bind(target) : value;
141
+ },
142
+ }) as AsyncIterableIterator<T>;
143
+ return proxy;
75
144
  }
76
145
 
77
146
  /** Imperatively establish `principal` for the current flow (entry points only). */