@telorun/kernel 0.80.0 → 0.82.0

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.
Files changed (71) hide show
  1. package/dist/controller-loaders/npm-install-root.d.ts +56 -0
  2. package/dist/controller-loaders/npm-install-root.d.ts.map +1 -0
  3. package/dist/controller-loaders/npm-install-root.js +109 -0
  4. package/dist/controller-loaders/npm-install-root.js.map +1 -0
  5. package/dist/controller-loaders/npm-loader.d.ts +3 -0
  6. package/dist/controller-loaders/npm-loader.d.ts.map +1 -1
  7. package/dist/controller-loaders/npm-loader.js +11 -1
  8. package/dist/controller-loaders/npm-loader.js.map +1 -1
  9. package/dist/controllers/logging/console-sink-controller.d.ts.map +1 -1
  10. package/dist/controllers/logging/console-sink-controller.js +19 -9
  11. package/dist/controllers/logging/console-sink-controller.js.map +1 -1
  12. package/dist/controllers/logging/file-sink-controller.d.ts.map +1 -1
  13. package/dist/controllers/logging/file-sink-controller.js +17 -6
  14. package/dist/controllers/logging/file-sink-controller.js.map +1 -1
  15. package/dist/controllers/module/import-controller.d.ts.map +1 -1
  16. package/dist/controllers/module/import-controller.js +36 -15
  17. package/dist/controllers/module/import-controller.js.map +1 -1
  18. package/dist/controllers/resource-definition/resource-template-controller.d.ts.map +1 -1
  19. package/dist/controllers/resource-definition/resource-template-controller.js +7 -5
  20. package/dist/controllers/resource-definition/resource-template-controller.js.map +1 -1
  21. package/dist/effect-scope.d.ts +132 -0
  22. package/dist/effect-scope.d.ts.map +1 -0
  23. package/dist/effect-scope.js +253 -0
  24. package/dist/effect-scope.js.map +1 -0
  25. package/dist/evaluation-context.d.ts +26 -1
  26. package/dist/evaluation-context.d.ts.map +1 -1
  27. package/dist/evaluation-context.js +156 -12
  28. package/dist/evaluation-context.js.map +1 -1
  29. package/dist/init-failure-diagnostics.d.ts +10 -0
  30. package/dist/init-failure-diagnostics.d.ts.map +1 -1
  31. package/dist/init-failure-diagnostics.js +7 -1
  32. package/dist/init-failure-diagnostics.js.map +1 -1
  33. package/dist/kernel.d.ts +6 -2
  34. package/dist/kernel.d.ts.map +1 -1
  35. package/dist/kernel.js +23 -14
  36. package/dist/kernel.js.map +1 -1
  37. package/dist/manifest-schemas.d.ts +4 -1
  38. package/dist/manifest-schemas.d.ts.map +1 -1
  39. package/dist/manifest-schemas.js +4 -10
  40. package/dist/manifest-schemas.js.map +1 -1
  41. package/dist/module-context.d.ts +10 -0
  42. package/dist/module-context.d.ts.map +1 -1
  43. package/dist/module-context.js +15 -0
  44. package/dist/module-context.js.map +1 -1
  45. package/dist/observed-state.d.ts.map +1 -1
  46. package/dist/observed-state.js +2 -4
  47. package/dist/observed-state.js.map +1 -1
  48. package/dist/resource-context.d.ts +18 -1
  49. package/dist/resource-context.d.ts.map +1 -1
  50. package/dist/resource-context.js +27 -0
  51. package/dist/resource-context.js.map +1 -1
  52. package/dist/schema-compiled-values.d.ts +2 -1
  53. package/dist/schema-compiled-values.d.ts.map +1 -1
  54. package/dist/schema-compiled-values.js +23 -20
  55. package/dist/schema-compiled-values.js.map +1 -1
  56. package/package.json +4 -3
  57. package/src/controller-loaders/npm-install-root.ts +131 -0
  58. package/src/controller-loaders/npm-loader.ts +12 -1
  59. package/src/controllers/logging/console-sink-controller.ts +19 -9
  60. package/src/controllers/logging/file-sink-controller.ts +17 -6
  61. package/src/controllers/module/import-controller.ts +50 -27
  62. package/src/controllers/resource-definition/resource-template-controller.ts +14 -11
  63. package/src/effect-scope.ts +340 -0
  64. package/src/evaluation-context.ts +165 -10
  65. package/src/init-failure-diagnostics.ts +8 -1
  66. package/src/kernel.ts +26 -13
  67. package/src/manifest-schemas.ts +4 -9
  68. package/src/module-context.ts +16 -0
  69. package/src/observed-state.ts +2 -4
  70. package/src/resource-context.ts +32 -0
  71. package/src/schema-compiled-values.ts +35 -21
@@ -297,23 +297,44 @@ export async function create(
297
297
  );
298
298
  }
299
299
  }
300
- ctx.registerModuleImport(alias, targetModule, exportedKindSuffixes);
300
+ // The alias registrations are an EFFECT on the create frame, not bare calls:
301
+ // an import whose `init()` fails is discarded and re-created on the next pass,
302
+ // so an alias left registered would be re-registered against a module context
303
+ // that already has it — and the abandoned child context would linger with no
304
+ // owner. One effect, because the four registrations are one act: an alias
305
+ // resolving kinds through a module whose instances are gone is not a state
306
+ // this controller should be able to produce.
307
+ await ctx
308
+ .effect(`import alias ${alias}`, async () => {
309
+ ctx.registerModuleImport(alias, targetModule, exportedKindSuffixes);
301
310
 
302
- // Publish the child's exported instances to the parent so cross-module `!ref Alias.name`
303
- // (Phase 5 injection / boot targets) and `${{ resources.Alias.name }}` (CEL value-flow)
304
- // resolve. The gate is `exports.resources`; the child's terminal getter is read lazily —
305
- // it exists after this import's init() built the child's export table. Handing the parent
306
- // the child's TERMINAL getter (not a wrapper) keeps resolution O(1) across re-export hops.
307
- (ctx.moduleContext as ModuleContext).registerImportedScope(
308
- alias,
309
- exportedResourceNames,
310
- (name) => childCtx.getTerminalExport(name),
311
- );
312
- // Same for kinds: `kind: Alias.Kind` resolves through the child's exported-kind table,
313
- // covering both locally-defined and transitively re-exported kinds in O(1).
314
- (ctx.moduleContext as ModuleContext).registerImportedKindScope(alias, (suffix) =>
315
- childCtx.getExportedKind(suffix),
316
- );
311
+ // Publish the child's exported instances to the parent so cross-module `!ref Alias.name`
312
+ // (Phase 5 injection / boot targets) and `${{ resources.Alias.name }}` (CEL value-flow)
313
+ // resolve. The gate is `exports.resources`; the child's terminal getter is read lazily —
314
+ // it exists after this import's init() built the child's export table. Handing the parent
315
+ // the child's TERMINAL getter (not a wrapper) keeps resolution O(1) across re-export hops.
316
+ (ctx.moduleContext as ModuleContext).registerImportedScope(
317
+ alias,
318
+ exportedResourceNames,
319
+ (name) => childCtx.getTerminalExport(name),
320
+ );
321
+ // Same for kinds: `kind: Alias.Kind` resolves through the child's exported-kind table,
322
+ // covering both locally-defined and transitively re-exported kinds in O(1).
323
+ (ctx.moduleContext as ModuleContext).registerImportedKindScope(alias, (suffix) =>
324
+ childCtx.getExportedKind(suffix),
325
+ );
326
+
327
+ return {
328
+ result: undefined,
329
+ inverse: () => {
330
+ (ctx.moduleContext as ModuleContext).unregisterImport(alias);
331
+ // The child context goes with the alias: it was spawned for this
332
+ // import and nothing else can reach it once the alias is gone.
333
+ ctx.moduleContext.detachChild(child);
334
+ },
335
+ };
336
+ })
337
+ .perform();
317
338
 
318
339
  // Return a ResourceInstance whose snapshot() surfaces the exported values under
319
340
  // resources.<alias>: the import's variables/secrets plus each exported instance's own
@@ -342,17 +363,19 @@ export async function create(
342
363
  ...exported,
343
364
  };
344
365
  },
345
- init: async () => {
346
- await child.initializeResources();
347
- // Build this import's flattened export tables now that its own imports are
348
- // registered (leaves-first), so a re-export (`!ref Alias.name` / `Alias.Kind`)
349
- // copies the source import's terminal getter / canonical kind by reference —
350
- // O(1) resolution at any depth.
351
- childCtx.buildExportTable(exportEntries, kindEntries, targetModule);
352
- },
353
- teardown: async () => {
354
- await child.teardownResources();
355
- },
366
+ // The library's resources ARE this import's allocation, and tearing the
367
+ // child context down is what undoes it — so the two are one effect rather
368
+ // than an init/teardown pair the kernel had to trust were inverses.
369
+ init: (importCtx) =>
370
+ importCtx.effect("library resources", async () => {
371
+ await child.initializeResources();
372
+ // Build this import's flattened export tables now that its own imports are
373
+ // registered (leaves-first), so a re-export (`!ref Alias.name` / `Alias.Kind`)
374
+ // copies the source import's terminal getter / canonical kind by reference —
375
+ // O(1) resolution at any depth.
376
+ childCtx.buildExportTable(exportEntries, kindEntries, targetModule);
377
+ return { result: undefined, inverse: () => child.teardownResources() };
378
+ }),
356
379
  };
357
380
  }
358
381
 
@@ -205,15 +205,21 @@ export function createTemplateController(definition: {
205
205
  let registered = false;
206
206
 
207
207
  return {
208
- init: async () => {
209
- if (!registered) {
210
- for (const template of definition.resources ?? []) {
211
- childContext.registerManifest(expandSelf(template));
208
+ // The template's own resources are its allocation, and tearing the child
209
+ // context down is the inverse. `init()` still resumes rather than
210
+ // restarting, because a deferral (a child ref naming a sibling that has
211
+ // not initialized) keeps the instance: only a real failure discards it.
212
+ init: (templateCtx) =>
213
+ templateCtx.effect("template resources", async () => {
214
+ if (!registered) {
215
+ for (const template of definition.resources ?? []) {
216
+ childContext.registerManifest(expandSelf(template));
217
+ }
218
+ registered = true;
212
219
  }
213
- registered = true;
214
- }
215
- await childContext.initializeResources();
216
- },
220
+ await childContext.initializeResources();
221
+ return { result: undefined, inverse: () => childContext.teardownResources() };
222
+ }),
217
223
 
218
224
  ...(invokeTarget && {
219
225
  invoke: async (inputs: any) => {
@@ -267,9 +273,6 @@ export function createTemplateController(definition: {
267
273
  },
268
274
  }),
269
275
 
270
- teardown: async () => {
271
- await childContext.teardownResources();
272
- },
273
276
  };
274
277
  },
275
278
  };
@@ -0,0 +1,340 @@
1
+ import type {
2
+ EffectBody,
3
+ EffectChain,
4
+ EffectOutcome,
5
+ EffectResult,
6
+ Inverse,
7
+ ResourceInstance,
8
+ } from "@telorun/sdk";
9
+ import { RuntimeError } from "@telorun/sdk";
10
+
11
+ /**
12
+ * The inverse accumulator behind the effect chain.
13
+ *
14
+ * A stack of FRAMES, one per lifecycle entry. The kernel opens a frame at
15
+ * `create()` and at each `init()` / `run()`; a failure unwinds only the frame
16
+ * that failed, so a failed `init()` cannot revert what `create()` did and then
17
+ * retry against a resource whose construction was rolled back. Teardown unwinds
18
+ * every open frame, newest first; within a frame, last-in-first-out.
19
+ *
20
+ * Normative contract: `kernel/specs/revertible-effects.md`.
21
+ */
22
+ export type FrameLabel = "create" | "init" | "run";
23
+
24
+ interface Registration {
25
+ readonly reason: string;
26
+ readonly inverse: Inverse;
27
+ disposed: boolean;
28
+ }
29
+
30
+ interface Frame {
31
+ readonly label: FrameLabel;
32
+ readonly entries: Registration[];
33
+ }
34
+
35
+ /** One inverse that refused, kept with the reason its author gave the effect. */
36
+ export interface RecoveryFailure {
37
+ readonly reason: string;
38
+ readonly error: unknown;
39
+ }
40
+
41
+ /** One link of a chain: what to run, and what the author called it. */
42
+ interface Step {
43
+ readonly reason: string;
44
+ readonly body: EffectBody<unknown, unknown>;
45
+ }
46
+
47
+ /** One line for an inverse's refusal, quoted into a recovery aggregate. */
48
+ function errorText(err: unknown): string {
49
+ return err instanceof Error ? err.message : String(err);
50
+ }
51
+
52
+ function isAsyncGenerator(value: unknown): value is AsyncGenerator<Inverse, unknown, void> {
53
+ return (
54
+ typeof value === "object" &&
55
+ value !== null &&
56
+ typeof (value as AsyncGenerator).next === "function" &&
57
+ Symbol.asyncIterator in (value as object)
58
+ );
59
+ }
60
+
61
+ /**
62
+ * A lazy chain of steps against one scope.
63
+ *
64
+ * Immutable and value-like, so `chain.effect(...)` in a branch or a loop builds
65
+ * a new description rather than mutating a shared one. Deliberately NOT a
66
+ * thenable: an `async init()` would unwrap it and hand the kernel its last
67
+ * result instead of the chain.
68
+ */
69
+ class Chain<T> implements EffectChain<T> {
70
+ constructor(
71
+ private readonly scope: EffectScope,
72
+ private readonly steps: readonly Step[],
73
+ ) {}
74
+
75
+ effect<TNext>(reason: string, body: EffectBody<T, TNext>): EffectChain<TNext> {
76
+ return new Chain<TNext>(this.scope, [
77
+ ...this.steps,
78
+ { reason, body: body as EffectBody<unknown, unknown> },
79
+ ]);
80
+ }
81
+
82
+ perform(): Promise<EffectResult<T>> {
83
+ return this.scope.execute(this.steps) as Promise<EffectResult<T>>;
84
+ }
85
+
86
+ /** The steps, for the kernel executing a chain a controller returned. */
87
+ plan(): readonly Step[] {
88
+ return this.steps;
89
+ }
90
+ }
91
+
92
+ /** A chain produced by this kernel, as opposed to any other object a controller
93
+ * might return from `init()`. */
94
+ export function isEffectChain(value: unknown): value is Chain<unknown> {
95
+ return value instanceof Chain;
96
+ }
97
+
98
+ export class EffectScope {
99
+ /** Innermost last. `create` is opened with the scope, so there is always a
100
+ * frame to register onto. */
101
+ private readonly frames: Frame[] = [];
102
+
103
+ /** Set while this scope is unwinding. A generator body checks it at each step
104
+ * boundary and stops rather than allocating into a scope that is going away —
105
+ * the mid-boot SIGINT case. */
106
+ private unwinding = false;
107
+
108
+ /** Terminal: set by {@link unwindAll}. A closed scope has no frames and takes
109
+ * no new effects. */
110
+ private closed = false;
111
+
112
+ constructor(private readonly label: string) {
113
+ this.openFrame("create");
114
+ }
115
+
116
+ /** Refuse work against a scope that has already unwound. Raised BEFORE a
117
+ * forward body runs, so a late effect cannot allocate and then find it has
118
+ * nowhere to record the inverse. */
119
+ private assertOpen(what: string): void {
120
+ if (!this.closed) return;
121
+ throw new RuntimeError(
122
+ "ERR_EFFECT_SCOPE_CLOSED",
123
+ `${this.label}: '${what}' cannot run — this resource has been torn down. ` +
124
+ `An effect registered now would record an inverse nothing will ever run.`,
125
+ );
126
+ }
127
+
128
+ openFrame(label: FrameLabel): void {
129
+ this.frames.push({ label, entries: [] });
130
+ }
131
+
132
+ /** Start a chain. Nothing runs until it is executed. */
133
+ chain<T>(reason: string, body: EffectBody<void, T>): EffectChain<T> {
134
+ return new Chain<never>(this, []).effect(reason, body as EffectBody<never, T>);
135
+ }
136
+
137
+ /**
138
+ * Register an inverse for work performed elsewhere, without a forward body.
139
+ *
140
+ * The synchronous door onto the same accumulator, for a primitive that
141
+ * already returns its own inverse: `acquireHold` hands back a release
142
+ * closure, and its public signature is synchronous, so it cannot go through a
143
+ * chain.
144
+ */
145
+ register(reason: string, inverse: Inverse): () => Promise<void> {
146
+ this.assertOpen(reason);
147
+ const entry: Registration = { reason, inverse, disposed: false };
148
+ this.current().entries.push(entry);
149
+ return () => this.disposeEntries([entry]);
150
+ }
151
+
152
+ /**
153
+ * Run a chain's steps in order against the frame open NOW, threading each
154
+ * step's result into the next.
155
+ *
156
+ * A step that throws leaves every inverse produced so far on the frame — this
157
+ * does not unwind, because whether a partial `init()` is recovered-and-retried
158
+ * or torn down is the caller's decision, not this function's.
159
+ */
160
+ async execute(steps: readonly Step[]): Promise<EffectResult<unknown>> {
161
+ const registered: Registration[] = [];
162
+ const push = (reason: string, inverse: Inverse): void => {
163
+ const entry: Registration = { reason, inverse, disposed: false };
164
+ this.current().entries.push(entry);
165
+ registered.push(entry);
166
+ };
167
+
168
+ let value: unknown = undefined;
169
+ for (const step of steps) {
170
+ this.assertOpen(step.reason);
171
+ if (this.unwinding) {
172
+ throw new RuntimeError(
173
+ "ERR_EFFECT_SCOPE_CLOSING",
174
+ `${this.label}: '${step.reason}' not started because the resource is being torn down`,
175
+ );
176
+ }
177
+ const produced = (step.body as (input: unknown) => unknown)(value);
178
+ if (isAsyncGenerator(produced)) {
179
+ const iterator = produced as AsyncGenerator<Inverse, unknown, void>;
180
+ for (;;) {
181
+ if (this.unwinding) {
182
+ // Stop at the step boundary and let the generator run its own
183
+ // `finally`. What already yielded stays on the frame and is
184
+ // recovered by the unwind in progress.
185
+ await iterator.return?.(undefined as never);
186
+ throw new RuntimeError(
187
+ "ERR_EFFECT_SCOPE_CLOSING",
188
+ `${this.label}: '${step.reason}' stopped because the resource is being torn down`,
189
+ );
190
+ }
191
+ const next = await iterator.next();
192
+ if (next.done) {
193
+ value = next.value;
194
+ break;
195
+ }
196
+ push(step.reason, next.value);
197
+ }
198
+ } else {
199
+ const outcome = (await produced) as EffectOutcome<unknown>;
200
+ // No inverse means the step allocated nothing that outlives a failure —
201
+ // a chain is the sequencing structure for lifecycle work as well as the
202
+ // record of what to undo, so a step with nothing to undo registers
203
+ // nothing rather than a no-op that would read as an oversight.
204
+ if (outcome.inverse) push(step.reason, outcome.inverse);
205
+ value = outcome.result;
206
+ }
207
+ }
208
+
209
+ return { result: value, dispose: () => this.disposeEntries(registered) };
210
+ }
211
+
212
+ /**
213
+ * Unwind the innermost frame and close it, returning what refused.
214
+ *
215
+ * Failures are returned rather than thrown: the caller decides what a refusal
216
+ * means. Pre-retry recovery withholds the resource (retrying from a state that
217
+ * could not be rolled back is worse than not retrying); teardown aggregates
218
+ * and keeps going, so one throwing resource cannot strand the log sinks pinned
219
+ * to outlive it.
220
+ */
221
+ async unwindFrame(): Promise<RecoveryFailure[]> {
222
+ const frame = this.frames.pop();
223
+ if (!frame) return [];
224
+ return this.runInverses(frame.entries);
225
+ }
226
+
227
+ /**
228
+ * Unwind every open frame, innermost first, and CLOSE the scope.
229
+ *
230
+ * Terminal, because both callers are: teardown, and the discard of a resource
231
+ * whose `init()` failed (its replacement is built with a fresh context, so a
232
+ * fresh scope). A closed scope refuses new effects rather than accepting them
233
+ * onto a frame nothing will ever unwind — recording an inverse that will never
234
+ * run is the silent leak this whole mechanism exists to remove, and it is
235
+ * exactly the shape a detached task still settling after teardown produces.
236
+ */
237
+ async unwindAll(): Promise<RecoveryFailure[]> {
238
+ const failures: RecoveryFailure[] = [];
239
+ while (this.frames.length > 0) failures.push(...(await this.unwindFrame()));
240
+ this.closed = true;
241
+ return failures;
242
+ }
243
+
244
+ private async runInverses(entries: Registration[]): Promise<RecoveryFailure[]> {
245
+ const wasUnwinding = this.unwinding;
246
+ this.unwinding = true;
247
+ const failures: RecoveryFailure[] = [];
248
+ for (const entry of [...entries].reverse()) {
249
+ if (entry.disposed) continue;
250
+ entry.disposed = true;
251
+ try {
252
+ await entry.inverse();
253
+ } catch (error) {
254
+ failures.push({ reason: entry.reason, error });
255
+ }
256
+ }
257
+ this.unwinding = wasUnwinding;
258
+ return failures;
259
+ }
260
+
261
+ private async disposeEntries(entries: Registration[]): Promise<void> {
262
+ const failures = await this.runInverses(entries);
263
+ if (failures.length === 0) return;
264
+ // An explicit dispose HAS a caller, unlike an unwind — so it throws rather
265
+ // than being collected into someone else's aggregate. Every refusal travels,
266
+ // matching the init and teardown paths: a dispose covers a chain, so
267
+ // reporting the first and dropping the rest would hide the others exactly
268
+ // where more than one thing failed to roll back.
269
+ throw new RuntimeError(
270
+ "ERR_EFFECT_RECOVERY_FAILED",
271
+ `${this.label}: ${failures.length} inverse(s) refused: ` +
272
+ failures.map((f) => `'${f.reason}' (${errorText(f.error)})`).join(", "),
273
+ failures.map((f) => ({
274
+ severity: "error" as const,
275
+ message: `inverse '${f.reason}' refused: ${errorText(f.error)}`,
276
+ })),
277
+ );
278
+ }
279
+
280
+ private current(): Frame {
281
+ this.assertOpen("effect");
282
+ const frame = this.frames[this.frames.length - 1];
283
+ if (!frame) throw new RuntimeError("ERR_EFFECT_NO_FRAME", `${this.label}: no open effect frame`);
284
+ return frame;
285
+ }
286
+ }
287
+
288
+ /**
289
+ * Execute whatever a lifecycle method returned.
290
+ *
291
+ * A controller that allocates nothing returns nothing, so a non-chain return is
292
+ * not an error — but it is also not an inverse, which is why `init()` returning
293
+ * a chain it forgot to hand back fails loudly and immediately: nothing was
294
+ * allocated at all.
295
+ */
296
+ export async function executeReturnedChain(returned: unknown, scope?: EffectScope): Promise<void> {
297
+ if (!isEffectChain(returned)) return;
298
+ if (!scope) {
299
+ throw new RuntimeError(
300
+ "ERR_EFFECT_NO_SCOPE",
301
+ "a controller returned an effect chain from a resource with no effect scope",
302
+ );
303
+ }
304
+ await scope.execute(returned.plan());
305
+ }
306
+
307
+ /**
308
+ * What the teardown cascade needs from a resource's context: its inverses, and
309
+ * its detached-task drain.
310
+ *
311
+ * Structural rather than the context class itself, so the accumulator does not
312
+ * pull the whole `ResourceContextImpl` into every consumer — and so the drain
313
+ * stays visibly a *second* thing, since it waits for in-flight work rather than
314
+ * undoing anything.
315
+ */
316
+ export interface EffectOwner {
317
+ readonly effects: EffectScope;
318
+ drainDetached(): Promise<void>;
319
+ }
320
+
321
+ /**
322
+ * An instance's effect owner, recorded at the kernel's single
323
+ * instance-production site — the anchor that already carries handle minting and
324
+ * contract binding, so an instance is never observable without one.
325
+ *
326
+ * A WeakMap rather than a field on the resource-instances map: teardown holds
327
+ * the instance and nothing else, and every reader of that map would otherwise
328
+ * have to learn about effects. First bind wins, mirroring handle minting: a
329
+ * `base:` child IS the parent instance returned verbatim, and re-binding would
330
+ * give one object two accumulators.
331
+ */
332
+ const owners = new WeakMap<ResourceInstance, EffectOwner>();
333
+
334
+ export function bindEffectOwner(instance: ResourceInstance, owner: EffectOwner): void {
335
+ if (!owners.has(instance)) owners.set(instance, owner);
336
+ }
337
+
338
+ export function effectOwnerOf(instance: ResourceInstance): EffectOwner | undefined {
339
+ return owners.get(instance);
340
+ }