@zackbart/connecta 0.24.4 → 0.26.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 (155) hide show
  1. package/AGENTS.md +43 -12
  2. package/CHANGELOG.md +524 -0
  3. package/README.md +32 -15
  4. package/SECURITY.md +6 -5
  5. package/bin/connecta.mjs +5 -1
  6. package/dist/activity.d.ts +10 -3
  7. package/dist/activity.js +10 -1
  8. package/dist/advertised-schema.js +53 -0
  9. package/dist/artifacts/connector.d.ts +55 -0
  10. package/dist/artifacts/connector.js +724 -0
  11. package/dist/artifacts/document.js +76 -0
  12. package/dist/artifacts/guide.js +140 -0
  13. package/dist/artifacts/html-scan.js +329 -0
  14. package/dist/artifacts/json.js +68 -0
  15. package/dist/artifacts/kv-store.d.ts +20 -0
  16. package/dist/artifacts/kv-store.js +140 -0
  17. package/dist/artifacts/markdown.js +449 -0
  18. package/dist/artifacts/module.d.ts +37 -0
  19. package/dist/artifacts/module.js +60 -0
  20. package/dist/artifacts/operations.d.ts +195 -0
  21. package/dist/artifacts/operations.js +944 -0
  22. package/dist/artifacts/refresh.d.ts +53 -0
  23. package/dist/artifacts/refresh.js +235 -0
  24. package/dist/artifacts/routes.js +194 -0
  25. package/dist/artifacts/types.d.ts +214 -0
  26. package/dist/artifacts/types.js +22 -0
  27. package/dist/artifacts/validate.d.ts +62 -0
  28. package/dist/artifacts/validate.js +652 -0
  29. package/dist/artifacts.d.ts +6 -0
  30. package/dist/artifacts.js +9 -0
  31. package/dist/auth/downstream-oauth.js +490 -106
  32. package/dist/call-admission.js +139 -113
  33. package/dist/catalog-service.js +152 -106
  34. package/dist/catalog.js +297 -38
  35. package/dist/concurrency.js +24 -23
  36. package/dist/connector-access.d.ts +18 -4
  37. package/dist/connector-access.js +65 -20
  38. package/dist/connector-caller.d.ts +8 -0
  39. package/dist/connector-caller.js +19 -0
  40. package/dist/connector-scope.js +6 -37
  41. package/dist/connectors/api.d.ts +2 -2
  42. package/dist/connectors/remote-mcp.d.ts +6 -5
  43. package/dist/connectors/remote-mcp.js +330 -240
  44. package/dist/credential-contract.d.ts +9 -0
  45. package/dist/credential-rules.js +2 -2
  46. package/dist/credentials.d.ts +6 -1
  47. package/dist/credentials.js +45 -3
  48. package/dist/errors.d.ts +16 -1
  49. package/dist/errors.js +28 -0
  50. package/dist/execute.js +745 -282
  51. package/dist/executor-admission.d.ts +11 -1
  52. package/dist/executor-admission.js +145 -63
  53. package/dist/executors/quickjs-runtime.js +4 -2
  54. package/dist/executors/quickjs.js +255 -212
  55. package/dist/fix-prompt.js +37 -0
  56. package/dist/index.d.ts +72 -7
  57. package/dist/index.js +281 -26
  58. package/dist/invocation.js +371 -286
  59. package/dist/meta-tools.js +213 -94
  60. package/dist/module-contracts.d.ts +21 -1
  61. package/dist/oauth-callback-outcome.js +94 -0
  62. package/dist/oauth-sealing.js +29 -0
  63. package/dist/operator-ui/fix-prompts.js +87 -0
  64. package/dist/operator-ui/generated.js +2 -2
  65. package/dist/operator-ui/model.d.ts +103 -1
  66. package/dist/operator-ui/setup-commands.js +53 -0
  67. package/dist/operator-ui/view.js +128 -3
  68. package/dist/program-source.js +321 -0
  69. package/dist/providers/notion.js +2 -2
  70. package/dist/providers/revenuecat.js +32 -5
  71. package/dist/providers/stripe.d.ts +3 -2
  72. package/dist/providers/vercel.js +1 -1
  73. package/dist/registry.d.ts +43 -7
  74. package/dist/registry.js +434 -288
  75. package/dist/resumable.js +1291 -0
  76. package/dist/routes/activity.js +74 -154
  77. package/dist/routes/actor-labels.js +114 -0
  78. package/dist/routes/credentials.js +196 -163
  79. package/dist/routes/mcp.js +228 -112
  80. package/dist/routes/oauth-management.js +73 -71
  81. package/dist/routes/oauth.js +44 -16
  82. package/dist/routes/operator.js +83 -0
  83. package/dist/routes/shared.d.ts +25 -1
  84. package/dist/routes/shared.js +20 -2
  85. package/dist/routes/ui-data.js +211 -0
  86. package/dist/routes/ui.js +76 -35
  87. package/dist/run-journal.js +347 -0
  88. package/dist/runtime/admission.js +43 -0
  89. package/dist/runtime/call-admission.js +67 -0
  90. package/dist/runtime/connector-scope.js +72 -0
  91. package/dist/runtime/run.js +174 -0
  92. package/dist/runtime/services.js +130 -0
  93. package/dist/runtime/shared-read.js +106 -0
  94. package/dist/runtime/storage.js +66 -0
  95. package/dist/server.js +137 -89
  96. package/dist/skills.js +28 -9
  97. package/dist/storage/file.js +39 -2
  98. package/dist/storage/memory.js +28 -16
  99. package/dist/timeout.js +39 -43
  100. package/dist/tool-safety.d.ts +27 -1
  101. package/dist/tool-safety.js +31 -0
  102. package/dist/types.d.ts +45 -7
  103. package/dist/ui.d.ts +25 -4
  104. package/dist/ui.js +77 -167
  105. package/dist/url-safety.js +157 -0
  106. package/dist/version.d.ts +1 -1
  107. package/dist/version.js +1 -1
  108. package/documentation/architecture.md +367 -16
  109. package/documentation/auth.md +163 -2
  110. package/documentation/code-mode.md +274 -56
  111. package/documentation/meta-tools.md +200 -44
  112. package/ethos.md +46 -47
  113. package/examples/worker/AGENTS.md +6 -0
  114. package/examples/worker/README.md +224 -14
  115. package/examples/worker/src/d1-activity-row.ts +14 -2
  116. package/examples/worker/src/d1-activity.ts +4 -2
  117. package/examples/worker/src/d1-storage.ts +116 -0
  118. package/examples/worker/src/index.ts +42 -7
  119. package/examples/worker/src/r2-artifact-blobs.ts +29 -0
  120. package/examples/worker/wrangler.jsonc +21 -5
  121. package/package.json +16 -4
  122. package/templates/node/AGENTS.md +9 -2
  123. package/templates/node/README.md +31 -3
  124. package/templates/node/package.json +1 -1
  125. package/templates/node/src/index.ts +27 -0
  126. package/dist/auth/downstream-oauth.d.ts +0 -200
  127. package/dist/catalog-fingerprint.d.ts +0 -19
  128. package/dist/catalog-limits.d.ts +0 -9
  129. package/dist/catalog-service.d.ts +0 -263
  130. package/dist/catalog.d.ts +0 -87
  131. package/dist/concurrency.d.ts +0 -3
  132. package/dist/connectors/guarded-fetch.d.ts +0 -96
  133. package/dist/credential-rules.d.ts +0 -85
  134. package/dist/execute.d.ts +0 -154
  135. package/dist/executor-result.d.ts +0 -10
  136. package/dist/executors/quickjs-child.d.ts +0 -1
  137. package/dist/executors/quickjs-protocol.d.ts +0 -67
  138. package/dist/favicon.d.ts +0 -1
  139. package/dist/identity.d.ts +0 -4
  140. package/dist/invocation.d.ts +0 -74
  141. package/dist/mcp-result.d.ts +0 -8
  142. package/dist/meta-tools.d.ts +0 -129
  143. package/dist/operator-ui/generated.d.ts +0 -2
  144. package/dist/operator-ui/view.d.ts +0 -188
  145. package/dist/result-shapes.d.ts +0 -13
  146. package/dist/retired-toolkits.d.ts +0 -2
  147. package/dist/routes/activity.d.ts +0 -2
  148. package/dist/routes/credentials.d.ts +0 -2
  149. package/dist/routes/mcp.d.ts +0 -10
  150. package/dist/routes/oauth-management.d.ts +0 -2
  151. package/dist/routes/oauth.d.ts +0 -2
  152. package/dist/routes/ui.d.ts +0 -2
  153. package/dist/server.d.ts +0 -9
  154. package/dist/skills.d.ts +0 -44
  155. package/dist/timeout.d.ts +0 -21
@@ -0,0 +1,72 @@
1
+ // The Effect face of connector scope teardown (src/connector-scope.ts).
2
+ //
3
+ // closeConnectorScope() is the Promise shell over closeScope() below. An
4
+ // Effect caller that owns a Scope registers the close as that scope's
5
+ // finalizer instead, so teardown runs however the work inside it ends.
6
+ import { Duration, Effect } from "effect";
7
+ import { detach } from "./run.js";
8
+ /** Enough for local transport abort/close without letting cleanup own latency. */
9
+ const CONNECTOR_SCOPE_CLOSE_BUDGET_MS = 100;
10
+ /**
11
+ * Bound on cleanup continued after the caller-facing window expires.
12
+ *
13
+ * `remoteMcp` spends at most one second asking the downstream to terminate its
14
+ * session, leaving another second for the local close. Custom hooks still get a
15
+ * finite background window: handing a never-settling promise to a Worker's
16
+ * `waitUntil` would otherwise keep the invocation alive until the platform cap.
17
+ */
18
+ const CONNECTOR_SCOPE_DEFER_BUDGET_MS = 2_000;
19
+ /**
20
+ * Succeed when `work` settles or `budgetMs` expires, whichever is first; never
21
+ * fail. The loser is interrupted, so a close that finishes early leaves no
22
+ * timer behind. Interrupting the wait leaves `work` running: nothing here can
23
+ * cancel a connector's close, only stop waiting for it.
24
+ */
25
+ function waitAtMost(work, budgetMs) {
26
+ return Effect.raceAllFirst([
27
+ Effect.promise(() => work),
28
+ Effect.sleep(Duration.millis(budgetMs)),
29
+ ]);
30
+ }
31
+ /**
32
+ * Tell a connector that a scope owned by the core has ended, with the contract
33
+ * closeConnectorScope() documents: best-effort, never failing, waited on for a
34
+ * small fixed window, with a bounded tail handed to `defer` when the runtime
35
+ * has one. The hook is called when the effect runs, not when it is built.
36
+ */
37
+ export function closeScope(connector, ctx, defer) {
38
+ return Effect.suspend(() => {
39
+ let settled;
40
+ try {
41
+ const closing = connector.closeScope?.(ctx);
42
+ if (!closing)
43
+ return Effect.void;
44
+ // Attach both handlers before either timer can win, so a late rejection
45
+ // is consumed rather than becoming an unhandled rejection.
46
+ settled = closing.then(() => { }, () => { });
47
+ }
48
+ catch {
49
+ // The scope is over whether or not the connector managed to clean it up.
50
+ return Effect.void;
51
+ }
52
+ if (defer) {
53
+ try {
54
+ detach(waitAtMost(settled, CONNECTOR_SCOPE_DEFER_BUDGET_MS), {
55
+ waitUntil: defer,
56
+ });
57
+ }
58
+ catch {
59
+ // A runtime hook is best-effort too; the caller cap still applies.
60
+ }
61
+ }
62
+ return waitAtMost(settled, CONNECTOR_SCOPE_CLOSE_BUDGET_MS);
63
+ });
64
+ }
65
+ /**
66
+ * Close the connector scope when the enclosing Scope closes — on success,
67
+ * failure, or interruption alike. Callers still own the at-most-once
68
+ * guarantee: register the finalizer once per connector scope.
69
+ */
70
+ export function closeScopeOnExit(connector, ctx, defer) {
71
+ return Effect.addFinalizer(() => closeScope(connector, ctx, defer));
72
+ }
@@ -0,0 +1,174 @@
1
+ // The one place an Effect becomes a Promise.
2
+ //
3
+ // Connecta's published surface is Promise-shaped and stays that way; Effect is
4
+ // an implementation detail behind it. Every edge where a fiber starts runs
5
+ // through this module, so the rules for that crossing live in one file instead
6
+ // of being re-derived at each call site — and test/purity.test.ts fails if any
7
+ // other file under src/ calls Effect.run*, runFork, forkDaemon, or
8
+ // ManagedRuntime.make.
9
+ //
10
+ // Two things the stock runners get wrong for this codebase:
11
+ //
12
+ // - The default MixedScheduler yields through setImmediate, which vitest's
13
+ // fake timers replace. A fiber that yields under vi.useFakeTimers() then
14
+ // waits for a timer tick nobody advances. Yielding through a microtask keeps
15
+ // fibers moving under fakes, and Workers never had setImmediate to begin
16
+ // with. The cost is that a fiber never yields to I/O, which is why CPU-heavy
17
+ // work stays out of Effect loops.
18
+ // - Effect.runPromise rejects an interrupted fiber with a generic "All fibers
19
+ // interrupted without error". Callers of withDeadline today see the abort
20
+ // reason the caller chose, and runEdge keeps it that way.
21
+ import { Cause, Context, Duration, Effect, Exit, ManagedRuntime, Scheduler, } from "effect";
22
+ /** Yield to the scheduler through the microtask queue, never a timer. */
23
+ function setMicrotask(task) {
24
+ let cancelled = false;
25
+ queueMicrotask(() => {
26
+ if (!cancelled)
27
+ task();
28
+ });
29
+ return () => {
30
+ cancelled = true;
31
+ };
32
+ }
33
+ /** The scheduler every edge-run fiber uses; see the header for why. */
34
+ export const microtaskScheduler = new Scheduler.MixedScheduler("async", setMicrotask);
35
+ const runExit = Effect.runPromiseExitWith(Context.make(Scheduler.Scheduler, microtaskScheduler));
36
+ function abortReason(signal) {
37
+ return signal?.reason ?? new DOMException("aborted", "AbortError");
38
+ }
39
+ /**
40
+ * Turn a failed Exit back into the value a Promise caller would have seen.
41
+ *
42
+ * An interrupt caused by the caller's signal wins, as the abort does in
43
+ * withDeadline's race. Otherwise the first typed failure is rethrown as is,
44
+ * then the first defect — never a wrapper, because callers check `instanceof`,
45
+ * `name`, and `message` on connecta's own error classes.
46
+ */
47
+ function rethrow(cause, signal) {
48
+ const reasons = cause.reasons;
49
+ if (signal?.aborted && reasons.some(Cause.isInterruptReason)) {
50
+ throw abortReason(signal);
51
+ }
52
+ const failure = reasons.find(Cause.isFailReason);
53
+ if (failure)
54
+ throw failure.error;
55
+ const defect = reasons.find(Cause.isDieReason);
56
+ if (defect)
57
+ throw defect.defect;
58
+ throw abortReason(signal);
59
+ }
60
+ const disposedRuntimes = new WeakSet();
61
+ /** Wrap a layer as an EdgeRuntime without building it. */
62
+ export function makeEdgeRuntime(layer) {
63
+ return ManagedRuntime.make(layer);
64
+ }
65
+ /**
66
+ * Release a runtime's layer resources. Idempotent, and never rejects: a
67
+ * finalizer that fails has nobody left to report to.
68
+ */
69
+ export async function disposeEdgeRuntime(runtime) {
70
+ if (disposedRuntimes.has(runtime))
71
+ return;
72
+ disposedRuntimes.add(runtime);
73
+ // Through runExit rather than runtime.dispose(), whose runner yields on
74
+ // Effect's default scheduler — the one vitest's fake timers freeze.
75
+ await runExit(runtime.disposeEffect);
76
+ }
77
+ /**
78
+ * The effect with the runtime's services provided, building them first when
79
+ * no run has yet. The build runs on the calling fiber's scheduler, so it is
80
+ * the microtask scheduler here, as everything else is.
81
+ */
82
+ function provideRuntime(effect, runtime) {
83
+ if (disposedRuntimes.has(runtime)) {
84
+ return Effect.die(new Error("This Connecta has been closed."));
85
+ }
86
+ const built = runtime.cachedContext;
87
+ if (built)
88
+ return Effect.provideContext(effect, built);
89
+ return Effect.flatMap(runtime.contextEffect, (context) => Effect.provideContext(effect, context));
90
+ }
91
+ export async function runEdge(effect, options = {}) {
92
+ const exit = await runExit(options.runtime
93
+ ? provideRuntime(effect, options.runtime)
94
+ // Without a runtime the overloads admit only an effect that requires
95
+ // nothing, so there is nothing to provide.
96
+ : effect, options.signal ? { signal: options.signal } : undefined);
97
+ if (Exit.isSuccess(exit))
98
+ return exit.value;
99
+ return rethrow(exit.cause, options.signal);
100
+ }
101
+ /**
102
+ * Fail with the signal's abort reason once it aborts; never succeed.
103
+ *
104
+ * Race it against work that should stop when the signal does. A signal
105
+ * aborted without a reason fails with an AbortError, as `fetch` would.
106
+ */
107
+ export function fromSignal(signal) {
108
+ return Effect.callback((resume) => {
109
+ // Remove the listener on every path, firing included. `once` already
110
+ // drops it from a real AbortSignal, but the cleanup below runs only on
111
+ // interruption, and a signal-shaped wrapper sees no removal otherwise.
112
+ const onAbort = () => {
113
+ signal.removeEventListener("abort", onAbort);
114
+ resume(Effect.fail(abortReason(signal)));
115
+ };
116
+ if (signal.aborted) {
117
+ onAbort();
118
+ return;
119
+ }
120
+ signal.addEventListener("abort", onAbort, { once: true });
121
+ return Effect.sync(() => signal.removeEventListener("abort", onAbort));
122
+ });
123
+ }
124
+ /**
125
+ * The Effect form of withDeadline (src/timeout.ts), with the same contract.
126
+ *
127
+ * The operation gets a signal of its own. The caller's signal is forwarded to
128
+ * it, and the deadline aborts it with the labelled `timeoutError` *before* the
129
+ * operation's fiber is interrupted, so work that honors the signal sees the
130
+ * same reason the caller does. Either abort fails the effect with that reason:
131
+ * the caller's, or `timeoutError`. The signal is always aborted on exit, as
132
+ * withDeadline's `finally` does.
133
+ *
134
+ * The error channel is `unknown` because an abort reason is whatever the
135
+ * caller chose.
136
+ */
137
+ export function withDeadlineEffect(operation, options) {
138
+ return Effect.suspend(() => {
139
+ const controller = new AbortController();
140
+ const forwardAbort = () => controller.abort(options.signal?.reason);
141
+ options.signal?.addEventListener("abort", forwardAbort, { once: true });
142
+ if (options.signal?.aborted)
143
+ forwardAbort();
144
+ // One race, not a race nested in a race: every contender is a forked
145
+ // fiber, and forks are most of what a deadline costs (P1-S02 measured
146
+ // about 2µs per raceFirst). Fork order is unchanged — operation, abort,
147
+ // timer — and the timer still aborts the signal before anything is
148
+ // interrupted, so the abort contender wins with the labelled reason.
149
+ const contenders = [
150
+ Effect.suspend(() => operation(controller.signal)),
151
+ fromSignal(controller.signal),
152
+ ];
153
+ if (options.timeoutMs !== undefined) {
154
+ contenders.push(Effect.sleep(Duration.millis(options.timeoutMs)).pipe(Effect.andThen(Effect.sync(() => controller.abort(options.timeoutError))), Effect.andThen(Effect.never)));
155
+ }
156
+ return Effect.raceAllFirst(contenders).pipe(Effect.ensuring(Effect.sync(() => {
157
+ controller.abort();
158
+ options.signal?.removeEventListener("abort", forwardAbort);
159
+ })));
160
+ });
161
+ }
162
+ /**
163
+ * The only sanctioned fire-and-forget.
164
+ *
165
+ * On Workers the work is handed to `ctx.waitUntil`, so the platform keeps the
166
+ * invocation alive until it settles rather than freezing it mid-flight. The
167
+ * returned promise never rejects: the outcome is the effect's own business,
168
+ * and an unhandled rejection from background work helps nobody.
169
+ */
170
+ export function detach(effect, ctx) {
171
+ const settled = runExit(effect).then(() => { }, () => { });
172
+ ctx?.waitUntil?.(settled);
173
+ return settled;
174
+ }
@@ -0,0 +1,130 @@
1
+ // The core's services, as Effect code sees them.
2
+ //
3
+ // createConnecta resolves storage, the vault, activity history, logging, and
4
+ // the rest of its configuration once, at construction. This module gives each
5
+ // of those a service key and assembles them into the layer behind that
6
+ // Connecta's runtime (an EdgeRuntime, src/runtime/run.ts), so an Effect
7
+ // program reads them from its context rather than from arguments threaded
8
+ // through every call. The shells — Registry, CatalogService, and the rest —
9
+ // stay constructible from plain arguments, as the tests construct them. The
10
+ // registry's storage programs read Storage and Logger too, but from their own
11
+ // partition rather than this runtime (see src/runtime/storage.ts for why).
12
+ //
13
+ // Every import from an optional module is type-only. An omitted activity
14
+ // module is a no-op recorder that builds no event and calls nothing, and the
15
+ // UI, vault, and activity implementations stay out of the root import graph
16
+ // (test/purity.test.ts).
17
+ import { Context, Effect, Layer } from "effect";
18
+ import { makeEdgeRuntime } from "./run.js";
19
+ /**
20
+ * The deployment's KVStorage: `config.storage`, or the memoryStorage() that
21
+ * stands in for it. `compareAndSet` is present exactly when the adapter
22
+ * implements it, so a caller that needs atomicity checks for it here.
23
+ */
24
+ export class Storage extends Context.Service()("connecta/Storage") {
25
+ }
26
+ /**
27
+ * The credential vault, or undefined when the deployment configures none —
28
+ * a required service whose value may be absent, not a default, so a program
29
+ * run without it provided fails to typecheck instead of finding no vault.
30
+ */
31
+ export class Vault extends Context.Service()("connecta/Vault") {
32
+ }
33
+ /**
34
+ * Connecta's diagnostic logger, already resolved: the configured Logger, a
35
+ * no-op for `"silent"`, or console output prefixed `[connecta]`. Effect's
36
+ * own logger is never used (test/purity.test.ts), because it honors neither
37
+ * `"silent"` nor the line format a deployment greps for.
38
+ */
39
+ export class Logger extends Context.Service()("connecta/Logger") {
40
+ }
41
+ /** Payload-free activity history, or a recorder that records nothing. */
42
+ export class ActivityRecorder extends Context.Service()("connecta/ActivityRecorder") {
43
+ }
44
+ /** What createConnecta resolved at construction. */
45
+ export class ResolvedConfig extends Context.Service()("connecta/ResolvedConfig") {
46
+ }
47
+ /**
48
+ * The request's hook for work that may outlive its response (a Worker's
49
+ * `ctx.waitUntil`). Per request, not per Connecta, so it is never part of the
50
+ * runtime: a request provides it with `Effect.provideService`, and a program
51
+ * outside any request sees undefined and keeps its work in the foreground.
52
+ */
53
+ export const DeferredWork = Context.Reference("connecta/DeferredWork", { defaultValue: () => undefined });
54
+ /**
55
+ * Resolve `ConnectaConfig.logger`. `"silent"` discards every line; an omitted
56
+ * logger writes to the console with the `[connecta]` prefix.
57
+ */
58
+ export function resolveLogger(logger) {
59
+ if (logger === "silent") {
60
+ return { debug() { }, info() { }, warn() { }, error() { } };
61
+ }
62
+ return logger ?? {
63
+ debug: (...a) => console.debug("[connecta]", ...a),
64
+ info: (...a) => console.info("[connecta]", ...a),
65
+ warn: (...a) => console.warn("[connecta]", ...a),
66
+ error: (...a) => console.error("[connecta]", ...a),
67
+ };
68
+ }
69
+ const NO_ACTIVITY = {
70
+ enabled: false,
71
+ recordTool: () => Effect.void,
72
+ recordDrift: () => Effect.void,
73
+ };
74
+ function activityRecorder(module, serverInfo, logger) {
75
+ const deployment = module.deploymentId !== undefined
76
+ ? { deploymentId: module.deploymentId }
77
+ : {};
78
+ // A custom module's recorder that throws is logged like a store that
79
+ // throws: activity never changes the outcome of the work it describes.
80
+ const bestEffort = (record) => Effect.sync(() => {
81
+ try {
82
+ record();
83
+ }
84
+ catch (error) {
85
+ logger.warn("[connecta] activity record failed", error);
86
+ }
87
+ });
88
+ return {
89
+ enabled: true,
90
+ recordTool: (request, input) => DeferredWork.use((defer) => bestEffort(() => module.recordTool({
91
+ sink: module.store,
92
+ recordTool: module.recordTool,
93
+ actor: request.actor,
94
+ requestId: request.requestId,
95
+ serverInfo,
96
+ ...deployment,
97
+ ...(defer ? { defer } : {}),
98
+ logger,
99
+ }, input))),
100
+ recordDrift: (input) => bestEffort(() => module.recordDrift({ sink: module.store, serverInfo, ...deployment, logger }, input)),
101
+ };
102
+ }
103
+ /**
104
+ * The layer behind a Connecta's runtime. Every service is a value resolved
105
+ * before this is called, so building the layer only assembles a context: it
106
+ * starts no fiber and touches no storage.
107
+ */
108
+ function coreServices(options) {
109
+ const activity = options.activity
110
+ ? activityRecorder(options.activity, options.config.serverInfo, options.logger)
111
+ : NO_ACTIVITY;
112
+ return Layer.succeedContext(Context.make(Storage, options.storage).pipe(Context.add(Vault, options.vault), Context.add(Logger, options.logger), Context.add(ActivityRecorder, activity), Context.add(ResolvedConfig, options.config)));
113
+ }
114
+ // Keyed by the root Registry: it is the per-Connecta object every internal
115
+ // caller already holds, and a Registry built directly (as the tests build
116
+ // them) simply has no runtime.
117
+ const runtimes = new WeakMap();
118
+ /**
119
+ * Create the runtime for the Connecta that owns `registry`. Runs nothing:
120
+ * the layer is built by the first run that needs it (see EdgeRuntime).
121
+ */
122
+ export function createCoreRuntime(registry, options) {
123
+ const runtime = makeEdgeRuntime(coreServices(options));
124
+ runtimes.set(registry, runtime);
125
+ return runtime;
126
+ }
127
+ /** The runtime createConnecta made for this root registry, if it made one. */
128
+ export function coreRuntime(registry) {
129
+ return runtimes.get(registry);
130
+ }
@@ -0,0 +1,106 @@
1
+ // One read that several readers in one request wait on, each on its own terms.
2
+ //
3
+ // Discovery and the registry both coalesce a connector's catalog read inside
4
+ // one request, so a search, a describe, and the call after them cost one
5
+ // downstream listing. The read used to run under whichever reader asked
6
+ // first: its signal was the read's signal, so a short-deadline call that
7
+ // happened to start the read failed a search that joined it with the call's
8
+ // timeout, and a cancelled search cancelled a read others were still waiting
9
+ // on (#571).
10
+ //
11
+ // So the read owns its signal. Each reader waits under its own cancellation,
12
+ // and the read is cancelled only when no reader is left to want it. Nothing
13
+ // here is shared across requests: a SharedRead lives in one request's map and
14
+ // is waited on only by that request's fibers, which is why a plain count of
15
+ // readers is enough and no edge is needed between them.
16
+ import { Deferred, Effect, Exit } from "effect";
17
+ export class SharedRead {
18
+ onEnd;
19
+ controller = new AbortController();
20
+ outcome = Deferred.makeUnsafe();
21
+ readers = 0;
22
+ ended = false;
23
+ /**
24
+ * Start `read` at once, under a signal the read owns. `onEnd` runs once:
25
+ * when the read settles, with whether it succeeded, or when it is cancelled
26
+ * first, with `false`. It runs before any reader hears the outcome, so an
27
+ * owner that evicts the read there sends a reader who retries on hearing of
28
+ * a failure to a fresh read, never back to this one.
29
+ */
30
+ constructor(read, onEnd = () => { }) {
31
+ this.onEnd = onEnd;
32
+ // The executor turns a synchronous throw from `read` into a rejection.
33
+ new Promise((resolve) => resolve(read(this.controller.signal))).then((value) => this.settle(Exit.succeed(value)), (cause) => this.settle(Exit.fail(cause)));
34
+ }
35
+ settle(exit) {
36
+ this.end(Exit.isSuccess(exit));
37
+ Deferred.doneUnsafe(this.outcome, exit);
38
+ }
39
+ end(succeeded) {
40
+ if (this.ended)
41
+ return;
42
+ this.ended = true;
43
+ this.onEnd(succeeded);
44
+ }
45
+ // Cancel the read for good. After this it is no longer joinable: whatever
46
+ // it eventually settles with still reaches the readers already waiting, but
47
+ // its owner has evicted it and a newcomer starts afresh.
48
+ cancel(reason) {
49
+ if (this.ended)
50
+ return;
51
+ this.controller.abort(reason);
52
+ this.end(false);
53
+ }
54
+ /**
55
+ * Wait for the read under this reader's own cancellation.
56
+ *
57
+ * A reader whose signal aborts while others still wait fails at once with
58
+ * its own reason and leaves the read to them. The last reader's abort is the
59
+ * read's: it is forwarded to the read's signal and that reader waits for the
60
+ * read's answer, as a lone reader always has, so a connector that finishes
61
+ * its listing anyway still hands it over. A reader that is interrupted — its
62
+ * deadline, as `withDeadlineEffect` ends it — simply leaves, and when it was
63
+ * the last one the read is cancelled with its reason.
64
+ */
65
+ join(signal) {
66
+ return Effect.suspend(() => {
67
+ this.readers++;
68
+ let waiting = true;
69
+ const leave = Effect.sync(() => {
70
+ if (!waiting)
71
+ return;
72
+ waiting = false;
73
+ this.readers--;
74
+ if (this.readers === 0)
75
+ this.cancel(signal?.reason);
76
+ });
77
+ const answer = Deferred.await(this.outcome);
78
+ const own = signal
79
+ ? Effect.raceFirst(answer, this.leftBy(signal))
80
+ : answer;
81
+ return Effect.ensuring(own, leave);
82
+ });
83
+ }
84
+ // Fails with the reader's abort reason when other readers remain; when it is
85
+ // the last, forwards the abort to the read and never completes, leaving the
86
+ // read's own answer to win the race.
87
+ leftBy(signal) {
88
+ return Effect.callback((resume) => {
89
+ const onAbort = () => {
90
+ signal.removeEventListener("abort", onAbort);
91
+ if (this.readers > 1) {
92
+ resume(Effect.fail(signal.reason));
93
+ }
94
+ else {
95
+ this.cancel(signal.reason);
96
+ }
97
+ };
98
+ if (signal.aborted) {
99
+ onAbort();
100
+ return;
101
+ }
102
+ signal.addEventListener("abort", onAbort, { once: true });
103
+ return Effect.sync(() => signal.removeEventListener("abort", onAbort));
104
+ });
105
+ }
106
+ }
@@ -0,0 +1,66 @@
1
+ // The Storage service, as the registry's programs use it.
2
+ //
3
+ // KVStorage stays Promise-shaped: it is what a deployment author implements,
4
+ // and nobody should need Effect to write a storage adapter. The operations
5
+ // below lift one call each into an effect that reads the adapter from the
6
+ // Storage service and fails with whatever the adapter threw, unwrapped, so a
7
+ // warning built from it reads exactly as it did.
8
+ //
9
+ // Which Storage a program sees is the registry's decision, not the runtime's.
10
+ // The Connecta runtime (src/runtime/services.ts) is keyed by the root
11
+ // Registry, and that is the wrong key here: a personal registry's storage is
12
+ // the root's namespaced to `principal:<key>:`, so handing its programs the
13
+ // runtime's Storage would read and write another partition. Each Registry —
14
+ // root, personal, or one a test built from plain arguments — therefore
15
+ // provides its own storage and logger to the programs it runs
16
+ // (`runOnPartition`). For the root registry those are the very values the
17
+ // runtime holds, because createConnecta passes the same two objects to both.
18
+ // A scoped view holds no storage of its own: it reaches Storage through the
19
+ // registry it delegates to, and the result stash is always the root's,
20
+ // because its capacity is runtime-wide.
21
+ import { Context, Effect } from "effect";
22
+ import { runEdge } from "./run.js";
23
+ import { Logger, Storage } from "./services.js";
24
+ /** Read one key; null when absent. */
25
+ export function storageGet(key) {
26
+ return Storage.use((storage) => Effect.tryPromise({
27
+ // Promise.resolve keeps an adapter that answers synchronously working,
28
+ // as `await` did; a synchronous throw is caught either way.
29
+ try: () => Promise.resolve(storage.get(key)),
30
+ catch: (error) => error,
31
+ }));
32
+ }
33
+ /** Write one key, with the adapter's optional TTL. */
34
+ export function storageSet(key, value, options) {
35
+ return Storage.use((storage) => Effect.tryPromise({
36
+ try: () => Promise.resolve(storage.set(key, value, options)),
37
+ catch: (error) => error,
38
+ }));
39
+ }
40
+ /** Delete one key. */
41
+ export function storageDelete(key) {
42
+ return Storage.use((storage) => Effect.tryPromise({
43
+ try: () => Promise.resolve(storage.delete(key)),
44
+ catch: (error) => error,
45
+ }));
46
+ }
47
+ // One context per partition object, built on first use. A registry's options
48
+ // object lives exactly as long as the registry, so the WeakMap never holds a
49
+ // partition past its owner.
50
+ const partitions = new WeakMap();
51
+ function partitionContext(partition) {
52
+ let context = partitions.get(partition);
53
+ if (!context) {
54
+ context = Context.make(Storage, partition.storage).pipe(Context.add(Logger, partition.logger));
55
+ partitions.set(partition, context);
56
+ }
57
+ return context;
58
+ }
59
+ /**
60
+ * Run a registry program against the registry's own storage and logger, at a
61
+ * Promise boundary. Resolves and rejects as runEdge does: the original error,
62
+ * never a wrapper.
63
+ */
64
+ export function runOnPartition(effect, partition) {
65
+ return runEdge(Effect.provideContext(effect, partitionContext(partition)));
66
+ }