@voltro/runtime 0.84.0 → 0.86.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 (160) hide show
  1. package/CHANGELOG.md +1215 -0
  2. package/THIRD-PARTY-NOTICES.md +118 -600
  3. package/dist/aggregate.js +42 -0
  4. package/dist/aggregateExtrema.js +80 -0
  5. package/dist/aggregateIvm.js +115 -0
  6. package/dist/analyticsMirror.js +228 -0
  7. package/dist/analyticsSink.js +38 -0
  8. package/dist/apiKeyUsage.js +33 -0
  9. package/dist/apiKeys.js +223 -0
  10. package/dist/apiKeysWiring.js +114 -0
  11. package/dist/approvalBuiltins.js +10 -0
  12. package/dist/approvals.js +269 -0
  13. package/dist/authMiddlewareLayer.js +47 -0
  14. package/dist/bindEvent.js +72 -0
  15. package/dist/bootListener.js +76 -0
  16. package/dist/bootRefusal.js +7 -0
  17. package/dist/bootStoreCodec.js +70 -0
  18. package/dist/businessRules.js +67 -0
  19. package/dist/changeClaimsTable.js +12 -0
  20. package/dist/circuitBreaker.js +72 -0
  21. package/dist/columnOnUpdate.js +23 -0
  22. package/dist/composeAnalytics.js +108 -0
  23. package/dist/computedQuery.js +4 -0
  24. package/dist/connectionBuiltins.js +81 -0
  25. package/dist/connectionCredentials.js +66 -0
  26. package/dist/connectionState.js +34 -0
  27. package/dist/connectionVault.js +410 -0
  28. package/dist/connectionsFacade.js +54 -0
  29. package/dist/coordinatedSchedule.js +139 -0
  30. package/dist/crdtHistory.js +138 -0
  31. package/dist/crdtMerge.js +179 -0
  32. package/dist/crdtVersionsTable.js +25 -0
  33. package/dist/credentialBound.js +12 -0
  34. package/dist/crud.js +72 -0
  35. package/dist/dataStore.js +679 -0
  36. package/dist/dataloader.js +61 -0
  37. package/dist/declaredTablesGuard.js +48 -0
  38. package/dist/defineExecutor.js +4 -0
  39. package/dist/dependencyGraph.js +32 -0
  40. package/dist/devices.js +228 -0
  41. package/dist/devicesWiring.js +168 -0
  42. package/dist/dialectReplicationAdapter.js +29 -0
  43. package/dist/dispatcher.js +447 -0
  44. package/dist/effectStore.js +71 -0
  45. package/dist/eventBus.js +260 -0
  46. package/dist/eventPublish.js +49 -0
  47. package/dist/events.js +193 -0
  48. package/dist/expectation.js +52 -0
  49. package/dist/expectationEvaluator.js +308 -0
  50. package/dist/experiment.js +96 -0
  51. package/dist/experimentEvaluator.js +245 -0
  52. package/dist/externalizedTasks.js +25 -0
  53. package/dist/fieldCipher.js +61 -0
  54. package/dist/fingerprint.js +21 -0
  55. package/dist/finops.js +42 -0
  56. package/dist/finopsAccountant.js +216 -0
  57. package/dist/handlerBody.js +8 -0
  58. package/dist/healthProbes.js +55 -0
  59. package/dist/httpClientSsrf.js +49 -0
  60. package/dist/httpClientTracing.js +11 -0
  61. package/dist/httpCompression.js +76 -0
  62. package/dist/httpKeepAlive.js +12 -0
  63. package/dist/httpRouteMounts.js +47 -0
  64. package/dist/idempotencyStore.js +70 -0
  65. package/dist/idleDetector.js +21 -0
  66. package/dist/index.d.ts +252 -233
  67. package/dist/index.js +144 -14405
  68. package/dist/insertRequiredColumns.js +19 -0
  69. package/dist/inspectWorkflow.js +1 -0
  70. package/dist/instanceMembership.js +98 -0
  71. package/dist/ivm.js +125 -0
  72. package/dist/kvDataStore.js +86 -0
  73. package/dist/kvFacade.js +32 -0
  74. package/dist/matcher.js +304 -0
  75. package/dist/memoryColumnDefaults.js +11 -0
  76. package/dist/memoryQueryJoins.js +114 -0
  77. package/dist/metrics.js +161 -0
  78. package/dist/observability.js +123 -0
  79. package/dist/observedGraph.js +52 -0
  80. package/dist/originGuard.js +116 -0
  81. package/dist/outbox.js +352 -0
  82. package/dist/pluginRefRules.js +65 -0
  83. package/dist/policyGuardResolver.js +55 -0
  84. package/dist/preview.js +62 -0
  85. package/dist/projectedRows.js +12 -0
  86. package/dist/provenance.js +63 -0
  87. package/dist/rangeIndex.js +89 -0
  88. package/dist/rawReadScope.js +17 -0
  89. package/dist/reaction.js +85 -0
  90. package/dist/reactiveConfig.js +11 -0
  91. package/dist/readClassification.js +57 -0
  92. package/dist/readRecorder.js +66 -0
  93. package/dist/rebac.js +104 -0
  94. package/dist/rebacInterceptor.js +81 -0
  95. package/dist/registry.js +29 -0
  96. package/dist/relevantFields.js +65 -0
  97. package/dist/replicaHealth.js +42 -0
  98. package/dist/replicaLocality.js +44 -0
  99. package/dist/replicaObservations.js +121 -0
  100. package/dist/replicaSelection.js +32 -0
  101. package/dist/replicatedDataStore.js +239 -0
  102. package/dist/requestLoader.js +7 -0
  103. package/dist/resumeCensus.js +28 -0
  104. package/dist/revalidation.js +22 -0
  105. package/dist/rowFilter.js +65 -0
  106. package/dist/rowStateBytes.js +45 -0
  107. package/dist/rpcBodyCap.js +63 -0
  108. package/dist/rpcConnections.js +83 -0
  109. package/dist/rpcDefectDiagnostics.js +15 -0
  110. package/dist/rpcServer.js +992 -0
  111. package/dist/runAsSystem.js +23 -0
  112. package/dist/rywPositionStore.js +84 -0
  113. package/dist/schedule.js +23 -0
  114. package/dist/scheduleBackfillRequests.js +151 -0
  115. package/dist/scheduleClaimsTable.js +11 -0
  116. package/dist/scheduleRunsTable.js +37 -0
  117. package/dist/scheduler.js +835 -0
  118. package/dist/schemaRegistry.js +65 -0
  119. package/dist/secretsResolver.js +35 -0
  120. package/dist/securityHeaders.js +33 -0
  121. package/dist/servePipeline.js +305 -0
  122. package/dist/serverOnlyAudit.js +66 -0
  123. package/dist/serverTracer.js +15 -0
  124. package/dist/serviceCredentials.js +40 -0
  125. package/dist/singleNodeOrchestrator.js +91 -0
  126. package/dist/sourceResolution.js +42 -0
  127. package/dist/storeMiddleware.js +880 -0
  128. package/dist/storeQuery.js +204 -0
  129. package/dist/storeRead.js +10 -0
  130. package/dist/subscriber.js +7 -0
  131. package/dist/subscriptionOutbox.js +139 -0
  132. package/dist/subscriptionRegistry.js +82 -0
  133. package/dist/subscriptionResume.js +132 -0
  134. package/dist/subscriptionSocketMetrics.js +65 -0
  135. package/dist/systemSubject.js +5 -0
  136. package/dist/tenantScope.js +23 -0
  137. package/dist/tenantSelection.js +27 -0
  138. package/dist/timeline.js +105 -0
  139. package/dist/traceContext.js +5 -0
  140. package/dist/traceSink.js +63 -0
  141. package/dist/traceSpansTable.js +29 -0
  142. package/dist/traceparent.js +34 -0
  143. package/dist/transactionSubject.js +12 -0
  144. package/dist/trustedProxy.js +155 -0
  145. package/dist/undo.js +129 -0
  146. package/dist/undoBuiltins.js +69 -0
  147. package/dist/undoCapture.js +74 -0
  148. package/dist/undoLogTable.js +16 -0
  149. package/dist/upsertBinding.js +20 -0
  150. package/dist/wakeOrchestrator.js +34 -0
  151. package/dist/wakeRegistry.js +71 -0
  152. package/dist/wakeRouter.js +63 -0
  153. package/dist/wakeSupervisor.js +62 -0
  154. package/dist/wakeupsTable.js +12 -0
  155. package/dist/webSocketGatewayRuntime.js +114 -0
  156. package/dist/webhookVerification.js +18 -0
  157. package/dist/wireColumnStrip.js +84 -0
  158. package/dist/workflowReadBuiltins.js +87 -0
  159. package/dist/workflowRuntime.js +708 -0
  160. package/package.json +10 -13
@@ -0,0 +1,308 @@
1
+ import { AggregateMaintainer as e } from "./aggregateIvm.js";
2
+ import { collectProjectedRows as t } from "./projectedRows.js";
3
+ //#region src/expectationEvaluator.ts
4
+ var n = (e) => {
5
+ if (e instanceof Date) return e.getTime();
6
+ if (typeof e == "number" && Number.isFinite(e)) return e;
7
+ if (typeof e == "string" && e.length > 0) {
8
+ let t = Date.parse(e);
9
+ return Number.isNaN(t) ? null : t;
10
+ }
11
+ return null;
12
+ }, r = class {
13
+ maxAgeMs;
14
+ timeDependent = !0;
15
+ maintainer;
16
+ column;
17
+ constructor(t, n) {
18
+ this.maxAgeMs = n, this.column = t, this.maintainer = new e({
19
+ op: "max",
20
+ column: t
21
+ });
22
+ }
23
+ project(e) {
24
+ if (e == null) return null;
25
+ let t = n(e[this.column]);
26
+ return t === null ? null : {
27
+ ...e,
28
+ [this.column]: t
29
+ };
30
+ }
31
+ seed(e) {
32
+ this.maintainer.seed(t(e, (e) => this.project(e)));
33
+ }
34
+ apply(e) {
35
+ let t = this.project(e.old), n = this.project(e.new);
36
+ return t === null && n === null ? [] : t === null ? this.maintainer.applyChange({
37
+ op: "insert",
38
+ old: null,
39
+ new: n
40
+ }) : n === null ? this.maintainer.applyChange({
41
+ op: "delete",
42
+ old: t,
43
+ new: null
44
+ }) : this.maintainer.applyChange({
45
+ op: "update",
46
+ old: t,
47
+ new: n
48
+ });
49
+ }
50
+ resolveRescan(e, n) {
51
+ this.maintainer.resolveRescan(e, t(n, (e) => this.project(e)));
52
+ }
53
+ read(e) {
54
+ let t = this.maintainer.materialize()[0]?.value ?? null;
55
+ if (t === null) return {
56
+ metric: null,
57
+ threshold: this.maxAgeMs,
58
+ violated: !1
59
+ };
60
+ let n = e - t;
61
+ return {
62
+ metric: n,
63
+ threshold: this.maxAgeMs,
64
+ violated: n > this.maxAgeMs
65
+ };
66
+ }
67
+ }, i = class {
68
+ matches;
69
+ reduce;
70
+ timeDependent = !1;
71
+ total = 0;
72
+ matching = 0;
73
+ constructor(e, t) {
74
+ this.matches = e, this.reduce = t;
75
+ }
76
+ seed(e) {
77
+ this.total = e.length, this.matching = e.reduce((e, t) => e + +!!this.matches(t), 0);
78
+ }
79
+ add(e) {
80
+ e != null && (this.total += 1, this.matches(e) && (this.matching += 1));
81
+ }
82
+ remove(e) {
83
+ e != null && (--this.total, this.matches(e) && --this.matching);
84
+ }
85
+ apply(e) {
86
+ return e.op === "insert" ? this.add(e.new) : e.op === "delete" ? this.remove(e.old) : (this.remove(e.old), this.add(e.new)), this.total < 0 && (this.total = 0), this.matching < 0 && (this.matching = 0), this.matching > this.total && (this.matching = this.total), [];
87
+ }
88
+ resolveRescan() {}
89
+ read() {
90
+ return this.reduce(this.total, this.matching);
91
+ }
92
+ }, a = (e) => e == null, o = (e, t, n) => {
93
+ if (a(e)) return !1;
94
+ let r = Number(e);
95
+ return Number.isNaN(r) ? !1 : t !== void 0 && r < t || n !== void 0 && r > n;
96
+ }, s = (e) => {
97
+ switch (e.kind) {
98
+ case "freshness": return new r(e.column, e.maxAgeMs);
99
+ case "nullRate": {
100
+ let t = e.column, n = e.maxRate;
101
+ return new i((e) => a(e[t]), (e, t) => {
102
+ let r = e > 0 ? t / e : 0;
103
+ return {
104
+ metric: r,
105
+ threshold: n,
106
+ violated: r > n
107
+ };
108
+ });
109
+ }
110
+ case "rowCount": {
111
+ let { min: t, max: n } = e;
112
+ return new i(() => !1, (e) => ({
113
+ metric: e,
114
+ threshold: n ?? t ?? null,
115
+ violated: t !== void 0 && e < t || n !== void 0 && e > n
116
+ }));
117
+ }
118
+ case "valueBounds": {
119
+ let { column: t, min: n, max: r, maxViolationRate: a } = e;
120
+ return new i((e) => o(e[t], n, r), (e, t) => {
121
+ if (a !== void 0) {
122
+ let n = e > 0 ? t / e : 0;
123
+ return {
124
+ metric: n,
125
+ threshold: a,
126
+ violated: n > a
127
+ };
128
+ }
129
+ return {
130
+ metric: t,
131
+ threshold: 0,
132
+ violated: t > 0
133
+ };
134
+ });
135
+ }
136
+ }
137
+ }, c = (e) => ({
138
+ table: e.table,
139
+ op: e.op,
140
+ traceId: e.traceId ?? null,
141
+ subjectId: e.subjectId ?? null,
142
+ procedure: e.procedure ?? null
143
+ }), l = (e, t) => !t || e.metric === null ? "unknown" : e.violated ? "violated" : "holding", u = class {
144
+ def;
145
+ deps;
146
+ engine;
147
+ now;
148
+ seeded = !1;
149
+ status = "unknown";
150
+ metric = null;
151
+ threshold = null;
152
+ since = null;
153
+ lastEvaluatedAt = null;
154
+ lastCause = null;
155
+ constructor(e, t) {
156
+ this.def = e, this.deps = t, this.engine = s(e.invariant), this.now = t.now ?? Date.now, this.threshold = null;
157
+ }
158
+ get timeDependent() {
159
+ return this.engine.timeDependent;
160
+ }
161
+ inScope(e) {
162
+ if (e == null) return !1;
163
+ let t = this.def.on.where;
164
+ return t === void 0 || t(e);
165
+ }
166
+ async seed() {
167
+ let e = (await this.deps.queryBase()).filter((e) => this.inScope(e));
168
+ this.engine.seed(e), this.seeded = !0;
169
+ let t = this.engine.read(this.now());
170
+ this.commitReading(t, !1, null);
171
+ }
172
+ async applyChange(e) {
173
+ if (e.table !== this.def.on.table) return null;
174
+ let t = e.op !== "insert" && e.old == null, n = e.op !== "delete" && e.new == null;
175
+ if (t || n) {
176
+ let t = (await this.deps.queryBase()).filter((e) => this.inScope(e));
177
+ return this.engine.seed(t), this.seeded = !0, this.commitReading(this.engine.read(this.now()), !0, c(e));
178
+ }
179
+ let r = this.inScope(e.old), i = this.inScope(e.new);
180
+ if (!r && !i) return null;
181
+ let a = {
182
+ op: e.op,
183
+ old: r ? e.old : null,
184
+ new: i ? e.new : null
185
+ }, o = this.engine.apply(a);
186
+ if (o.length > 0) {
187
+ let e = (await this.deps.queryBase()).filter((e) => this.inScope(e));
188
+ this.engine.resolveRescan(o, e);
189
+ }
190
+ return this.commitReading(this.engine.read(this.now()), !0, c(e));
191
+ }
192
+ tick() {
193
+ return this.seeded ? this.commitReading(this.engine.read(this.now()), !0, null) : null;
194
+ }
195
+ commitReading(e, t, n) {
196
+ let r = this.status, i = l(e, this.seeded);
197
+ if (this.metric = e.metric, this.threshold = e.threshold, this.lastEvaluatedAt = new Date(this.now()), i !== r) {
198
+ if (this.status = i, this.since = this.lastEvaluatedAt, i === "violated" && (this.lastCause = n), t && i === "violated" && r !== "violated") return {
199
+ kind: "violated",
200
+ state: this.state(),
201
+ cause: n
202
+ };
203
+ if (t && r === "violated" && i !== "violated") return {
204
+ kind: "recovered",
205
+ state: this.state(),
206
+ cause: null
207
+ };
208
+ }
209
+ return null;
210
+ }
211
+ state() {
212
+ return {
213
+ name: this.def.name,
214
+ table: this.def.on.table,
215
+ invariant: this.def.invariant.kind,
216
+ rule: { ...this.def.invariant },
217
+ severity: this.def.severity,
218
+ status: this.status,
219
+ metric: this.metric,
220
+ threshold: this.threshold,
221
+ since: this.since,
222
+ lastEvaluatedAt: this.lastEvaluatedAt,
223
+ lastCause: this.lastCause,
224
+ ...this.def.description === void 0 ? {} : { description: this.def.description }
225
+ };
226
+ }
227
+ }, d = (e) => {
228
+ let t = e.now ?? Date.now, n = e.log, r = /* @__PURE__ */ new Set(), i = /* @__PURE__ */ new Map(), a = (e) => {
229
+ for (let t of r) try {
230
+ t(e);
231
+ } catch (t) {
232
+ n.warn("expectation: transition listener threw", { name: e.state.name }, t);
233
+ }
234
+ }, o = (t) => async () => await e.store.query({
235
+ table: t,
236
+ predicate: void 0,
237
+ order: [],
238
+ take: void 0,
239
+ skip: void 0,
240
+ projection: void 0
241
+ }), s = /* @__PURE__ */ new Map(), c = !1;
242
+ for (let { definition: r } of e.discovered) {
243
+ if (i.has(r.name)) throw Error(`expectation: duplicate name '${r.name}' — names must be unique across all expectations`);
244
+ let e = new u(r, {
245
+ queryBase: o(r.on.table),
246
+ now: t,
247
+ ...n === void 0 ? {} : { log: n }
248
+ });
249
+ i.set(r.name, e);
250
+ let a = s.get(r.on.table) ?? [];
251
+ a.push(e), s.set(r.on.table, a), e.timeDependent && (c = !0);
252
+ }
253
+ (async () => {
254
+ for (let e of i.values()) try {
255
+ await e.seed();
256
+ } catch (e) {
257
+ n.warn("expectation: seed failed — will retry on reconcile", { err: String(e) });
258
+ }
259
+ })();
260
+ let l = e.store.onChange((e) => {
261
+ let t = s.get(e.table);
262
+ t !== void 0 && t.length !== 0 && (async () => {
263
+ for (let r of t) try {
264
+ let t = await r.applyChange(e);
265
+ t !== null && a(t);
266
+ } catch (e) {
267
+ n.warn("expectation: delta apply failed — reconcile will heal", { err: String(e) });
268
+ }
269
+ })();
270
+ }), d = () => {
271
+ if (c) for (let e of i.values()) {
272
+ if (!e.timeDependent) continue;
273
+ let t = e.tick();
274
+ t !== null && a(t);
275
+ }
276
+ }, f = null;
277
+ c && (f = setInterval(d, e.tickIntervalMs ?? 3e4), f.unref?.());
278
+ let p = setInterval(() => {
279
+ (async () => {
280
+ for (let e of i.values()) try {
281
+ await e.seed();
282
+ } catch {}
283
+ })();
284
+ }, e.reconcileIntervalMs ?? 3e5);
285
+ p.unref?.();
286
+ let m = (e) => {
287
+ let t = e ? [] : Array(i.size), n = 0;
288
+ return i.forEach((r) => {
289
+ let i = r.state();
290
+ (!e || i.status === "violated") && (t[n++] = i);
291
+ }), t;
292
+ };
293
+ return {
294
+ registry: {
295
+ snapshot: () => m(!1),
296
+ get: (e) => i.get(e)?.state() ?? null,
297
+ violations: () => m(!0),
298
+ subscribe: (e) => (r.add(e), () => r.delete(e))
299
+ },
300
+ detach: () => {
301
+ l(), f !== null && clearInterval(f), clearInterval(p), r.clear();
302
+ },
303
+ registered: [...i.keys()],
304
+ runTick: d
305
+ };
306
+ };
307
+ //#endregion
308
+ export { u as ExpectationEvaluator, d as attachExpectations };
@@ -0,0 +1,96 @@
1
+ import { Context as e, Effect as t } from "effect";
2
+ //#region src/experiment.ts
3
+ var n = "holdout", r = (e, t) => /* @__PURE__ */ Error(`defineExperiment(${JSON.stringify(e)}): ${t}`), i = (e) => {
4
+ if (e == null) return null;
5
+ if (typeof e == "number") return Number.isFinite(e) ? String(e) : null;
6
+ let t = String(e);
7
+ return t.length > 0 ? t : null;
8
+ }, a = (e) => {
9
+ let t = e.name;
10
+ if (typeof t != "string" || t.length === 0) throw Error("defineExperiment: a non-empty `name` is required");
11
+ if (e.on === void 0 || typeof e.on.table != "string" || e.on.table.length === 0) throw r(t, "on.table is required — the table whose writes drive the experiment");
12
+ if (e.on.where !== void 0 && typeof e.on.where != "function") throw r(t, "on.where, when present, must be a predicate function");
13
+ let a = e.subject !== void 0, o = e.variantFrom !== void 0;
14
+ if (a && o) throw r(t, "declare `subject` OR `variantFrom`, not both — they are two answers to \"which arm is this row in?\"");
15
+ let s = null, c = null;
16
+ if (o) {
17
+ if (typeof e.variantFrom == "string") {
18
+ if (e.variantFrom.length === 0) throw r(t, "variantFrom, when a column name, must be non-empty");
19
+ let n = e.variantFrom;
20
+ c = (e) => i(e[n]);
21
+ } else if (typeof e.variantFrom == "function") {
22
+ let t = e.variantFrom;
23
+ c = (e) => i(t(e));
24
+ } else throw r(t, "variantFrom must be a column name or a (row) => variantName function");
25
+ } else if (typeof e.subject == "string") {
26
+ if (e.subject.length === 0) throw r(t, "subject, when a column name, must be non-empty");
27
+ let n = e.subject;
28
+ s = (e) => i(e[n]);
29
+ } else if (typeof e.subject == "function") {
30
+ let t = e.subject;
31
+ s = (e) => i(t(e));
32
+ } else throw r(t, "subject is required — a column name or a (row) => key function (or use `variantFrom` to measure an arm something else assigned)");
33
+ if (!Array.isArray(e.variants) || e.variants.length < 2) throw r(t, "at least two variants are required (a control + one treatment)");
34
+ let l = /* @__PURE__ */ new Set(), u = [];
35
+ for (let n of e.variants) {
36
+ if (typeof n != "object" || !n || typeof n.name != "string" || n.name.length === 0) throw r(t, "each variant needs a non-empty `name`");
37
+ if (l.has(n.name)) throw r(t, `duplicate variant name ${JSON.stringify(n.name)}`);
38
+ l.add(n.name);
39
+ let e = n.weight ?? 1;
40
+ if (!(typeof e == "number" && Number.isFinite(e) && e > 0)) throw r(t, `variant ${JSON.stringify(n.name)} weight must be a positive, finite number`);
41
+ u.push({
42
+ name: n.name,
43
+ weight: e
44
+ });
45
+ }
46
+ let d = 0;
47
+ if (e.holdout !== void 0) {
48
+ if (c !== null && e.holdout > 0) throw r(t, `holdout is meaningless with \`variantFrom\` — a holdout is carved out AT ASSIGNMENT, and this experiment does not assign. Declare the holdout arm on whatever does (a flag variant named "${n}", say) and list it in \`variants\`.`);
49
+ if (!(typeof e.holdout == "number" && e.holdout >= 0 && e.holdout < 1)) throw r(t, "holdout, when present, must be a fraction in [0, 1)");
50
+ if (d = e.holdout, d > 0 && l.has("holdout")) throw r(t, `a variant may not be named ${JSON.stringify(n)} when holdout > 0 (reserved)`);
51
+ }
52
+ let f = e.metric;
53
+ if (f === void 0 || typeof f.kind != "string") throw r(t, "metric is required");
54
+ switch (f.kind) {
55
+ case "count": break;
56
+ case "sum":
57
+ case "avg":
58
+ if (typeof f.column != "string" || f.column.length === 0) throw r(t, `${f.kind} metric requires a numeric \`column\``);
59
+ break;
60
+ case "conversionRate":
61
+ if (typeof f.column != "string" || f.column.length === 0) throw r(t, "conversionRate metric requires a `column`");
62
+ break;
63
+ default: throw r(t, `unknown metric kind ${JSON.stringify(f.kind)}`);
64
+ }
65
+ let p = e.baseline ?? u[0].name;
66
+ if (!l.has(p)) throw r(t, `baseline ${JSON.stringify(p)} is not one of the declared variants`);
67
+ return {
68
+ _voltroExperiment: !0,
69
+ name: t,
70
+ on: e.on.where === void 0 ? { table: e.on.table } : {
71
+ table: e.on.table,
72
+ where: e.on.where
73
+ },
74
+ subject: s,
75
+ variantFrom: c,
76
+ variants: u,
77
+ holdout: d,
78
+ metric: f,
79
+ baseline: p,
80
+ ...e.description === void 0 ? {} : { description: e.description }
81
+ };
82
+ }, o = (e) => typeof e == "object" && !!e && e._voltroExperiment === !0, s = (e) => {
83
+ let t = 2166136261;
84
+ for (let n = 0; n < e.length; n++) t ^= e.charCodeAt(n), t = Math.imul(t, 16777619);
85
+ return (t >>> 0) / 4294967296;
86
+ }, c = (e, t) => {
87
+ let r = s(`${e.name}\u0000${t}`);
88
+ if (e.holdout > 0 && r < e.holdout) return n;
89
+ let i = e.holdout > 0 ? (r - e.holdout) / (1 - e.holdout) : r, a = e.variants.reduce((e, t) => e + t.weight, 0), o = 0;
90
+ for (let t of e.variants) if (o += t.weight / a, i < o) return t.name;
91
+ return e.variants[e.variants.length - 1].name;
92
+ }, l = (e) => e.holdout > 0 ? [...e.variants.map((e) => e.name), n] : e.variants.map((e) => e.name), u = class extends e.Service()("@voltro/ExperimentRegistry") {}, d = (e) => t.gen(function* () {
93
+ return (yield* u).get(e.name);
94
+ });
95
+ //#endregion
96
+ export { u as ExperimentRegistry, n as HOLDOUT_VARIANT, c as assignVariant, a as defineExperiment, s as hashUnitInterval, o as isExperimentDefinition, l as resultVariantNames, d as useExperiment };
@@ -0,0 +1,245 @@
1
+ import { AggregateMaintainer as e } from "./aggregateIvm.js";
2
+ import { collectProjectedRows as t } from "./projectedRows.js";
3
+ import { assignVariant as n, resultVariantNames as r } from "./experiment.js";
4
+ //#region src/experimentEvaluator.ts
5
+ var i = "__voltro_variant", a = "__voltro_metric", o = class {
6
+ project;
7
+ maintainer;
8
+ constructor(t, n) {
9
+ this.project = n, this.maintainer = new e(t);
10
+ }
11
+ seed(e) {
12
+ this.maintainer.seed(t(e, (e) => this.project(e)));
13
+ }
14
+ apply(e) {
15
+ let t = e.old ? this.project(e.old) : null, n = e.new ? this.project(e.new) : null;
16
+ (t !== null || n !== null) && (t === null ? this.maintainer.applyChange({
17
+ op: "insert",
18
+ old: null,
19
+ new: n
20
+ }) : n === null ? this.maintainer.applyChange({
21
+ op: "delete",
22
+ old: t,
23
+ new: null
24
+ }) : this.maintainer.applyChange({
25
+ op: "update",
26
+ old: t,
27
+ new: n
28
+ }));
29
+ }
30
+ byVariant() {
31
+ let e = /* @__PURE__ */ new Map();
32
+ for (let t of this.maintainer.materialize()) {
33
+ let n = t.group[i];
34
+ typeof n == "string" && e.set(n, t.value);
35
+ }
36
+ return e;
37
+ }
38
+ }, s = (e) => typeof e == "number" && Number.isFinite(e), c = (e) => {
39
+ if (s(e)) return e;
40
+ let t = Number(e);
41
+ return Number.isFinite(t) ? t : null;
42
+ }, l = (e, t) => new o(e.kind === "count" ? {
43
+ op: "count",
44
+ groupBy: [i]
45
+ } : {
46
+ op: e.kind === "sum" ? "sum" : "avg",
47
+ column: a,
48
+ groupBy: [i]
49
+ }, (n) => {
50
+ let r = t(n);
51
+ if (r === null) return null;
52
+ switch (e.kind) {
53
+ case "count": return { [i]: r };
54
+ case "sum":
55
+ case "avg": {
56
+ let t = c(n[e.column]);
57
+ return t === null ? null : {
58
+ [i]: r,
59
+ [a]: t
60
+ };
61
+ }
62
+ case "conversionRate": {
63
+ let t = n[e.column], o = e.equals === void 0 ? !!t : t === e.equals;
64
+ return {
65
+ [i]: r,
66
+ [a]: +!!o
67
+ };
68
+ }
69
+ }
70
+ }), u = (e) => new o({
71
+ op: "count",
72
+ groupBy: [i]
73
+ }, (t) => {
74
+ let n = e(t);
75
+ return n === null ? null : { [i]: n };
76
+ }), d = (e) => ({
77
+ table: e.table,
78
+ op: e.op,
79
+ traceId: e.traceId ?? null,
80
+ subjectId: e.subjectId ?? null,
81
+ procedure: e.procedure ?? null
82
+ }), f = class {
83
+ def;
84
+ deps;
85
+ exposure;
86
+ metric;
87
+ metricIsCount;
88
+ now;
89
+ seeded = !1;
90
+ lastUpdatedAt = null;
91
+ declaredVariants;
92
+ constructor(e, t) {
93
+ this.def = e, this.deps = t, this.now = t.now ?? Date.now, this.declaredVariants = new Set(r(e));
94
+ let n = (e) => this.variantOf(e);
95
+ this.exposure = u(n), this.metricIsCount = e.metric.kind === "count", this.metric = this.metricIsCount ? this.exposure : l(e.metric, n);
96
+ }
97
+ variantOf(e) {
98
+ if (e == null) return null;
99
+ let t = this.def.on.where;
100
+ if (t !== void 0 && !t(e)) return null;
101
+ if (this.def.variantFrom !== null) {
102
+ let t = this.def.variantFrom(e);
103
+ return t === null ? null : this.declaredVariants.has(t) ? t : null;
104
+ }
105
+ if (this.def.subject === null) return null;
106
+ let r = this.def.subject(e);
107
+ return r === null ? null : n(this.def, r);
108
+ }
109
+ inScope(e) {
110
+ return this.variantOf(e) !== null;
111
+ }
112
+ async seed() {
113
+ let e = await this.deps.queryBase();
114
+ this.exposure.seed(e), this.metricIsCount || this.metric.seed(e), this.seeded = !0, this.lastUpdatedAt = new Date(this.now());
115
+ }
116
+ applyChange(e) {
117
+ if (e.table !== this.def.on.table) return null;
118
+ let t = this.inScope(e.old), n = this.inScope(e.new);
119
+ if (!t && !n) return null;
120
+ let r = {
121
+ op: e.op,
122
+ old: t ? e.old : null,
123
+ new: n ? e.new : null
124
+ };
125
+ return this.exposure.apply(r), this.metricIsCount || this.metric.apply(r), this.lastUpdatedAt = new Date(this.now()), {
126
+ result: this.result(),
127
+ cause: d(e)
128
+ };
129
+ }
130
+ result() {
131
+ let e = this.exposure.byVariant(), t = this.metricIsCount ? e : this.metric.byVariant(), n = this.def.baseline, i = this.metricValueFor(n, t, e), a = 0, o = [];
132
+ for (let s of r(this.def)) {
133
+ let r = e.get(s) ?? 0;
134
+ a += r;
135
+ let c = this.metricValueFor(s, t, e), l = s === n, u = l || c === null || i === null ? null : c - i, d = l || c === null || i === null || i === 0 ? null : (c - i) / i;
136
+ o.push({
137
+ variant: s,
138
+ isBaseline: l,
139
+ isHoldout: this.def.holdout > 0 && s === "holdout",
140
+ exposure: r,
141
+ metric: c,
142
+ lift: d,
143
+ diff: u
144
+ });
145
+ }
146
+ return {
147
+ name: this.def.name,
148
+ table: this.def.on.table,
149
+ metric: this.def.metric.kind,
150
+ baseline: n,
151
+ totalExposure: a,
152
+ variants: o,
153
+ lastUpdatedAt: this.lastUpdatedAt,
154
+ ...this.def.description === void 0 ? {} : { description: this.def.description }
155
+ };
156
+ }
157
+ metricValueFor(e, t, n) {
158
+ let r = t.get(e);
159
+ if (r !== void 0) return r;
160
+ if (this.metricIsCount) return n.get(e) ?? 0;
161
+ switch (this.def.metric.kind) {
162
+ case "sum": return 0;
163
+ default: return null;
164
+ }
165
+ }
166
+ get isSeeded() {
167
+ return this.seeded;
168
+ }
169
+ }, p = (e) => {
170
+ let t = e.now ?? Date.now, n = e.log, r = /* @__PURE__ */ new Set(), i = /* @__PURE__ */ new Map(), a = (e) => {
171
+ for (let t of r) try {
172
+ t(e);
173
+ } catch (t) {
174
+ n.warn("experiment: update listener threw", { name: e.result.name }, t);
175
+ }
176
+ }, o = (t) => async () => await e.store.query({
177
+ table: t,
178
+ predicate: void 0,
179
+ order: [],
180
+ take: void 0,
181
+ skip: void 0,
182
+ projection: void 0
183
+ }), s = /* @__PURE__ */ new Map();
184
+ for (let { definition: r } of e.discovered) {
185
+ if (i.has(r.name)) throw Error(`experiment: duplicate name '${r.name}' — names must be unique across all experiments`);
186
+ let e = new f(r, {
187
+ queryBase: o(r.on.table),
188
+ now: t,
189
+ ...n === void 0 ? {} : { log: n }
190
+ });
191
+ i.set(r.name, e);
192
+ let a = s.get(r.on.table) ?? [];
193
+ a.push(e), s.set(r.on.table, a);
194
+ }
195
+ (async () => {
196
+ for (let e of i.values()) try {
197
+ await e.seed();
198
+ } catch (e) {
199
+ n.warn("experiment: seed failed — will retry on reconcile", { err: String(e) });
200
+ }
201
+ })();
202
+ let c = e.store.onChange((e) => {
203
+ let t = s.get(e.table);
204
+ if (t === void 0 || t.length === 0) return;
205
+ let r = e.op !== "insert" && e.old == null || e.op !== "delete" && e.new == null;
206
+ (async () => {
207
+ for (let i of t) try {
208
+ if (r) await i.seed(), a({
209
+ result: i.result(),
210
+ cause: d(e)
211
+ });
212
+ else {
213
+ let t = i.applyChange(e);
214
+ t !== null && a(t);
215
+ }
216
+ } catch (e) {
217
+ n.warn("experiment: delta apply failed — reconcile will heal", { err: String(e) });
218
+ }
219
+ })();
220
+ }), l = setInterval(() => {
221
+ (async () => {
222
+ for (let e of i.values()) try {
223
+ await e.seed();
224
+ } catch {}
225
+ })();
226
+ }, e.reconcileIntervalMs ?? 3e5);
227
+ return l.unref?.(), {
228
+ registry: {
229
+ snapshot: () => {
230
+ let e = Array(i.size), t = 0;
231
+ return i.forEach((n) => {
232
+ e[t++] = n.result();
233
+ }), e;
234
+ },
235
+ get: (e) => i.get(e)?.result() ?? null,
236
+ subscribe: (e) => (r.add(e), () => r.delete(e))
237
+ },
238
+ detach: () => {
239
+ c(), clearInterval(l), r.clear();
240
+ },
241
+ registered: [...i.keys()]
242
+ };
243
+ };
244
+ //#endregion
245
+ export { f as ExperimentEvaluator, p as attachExperiments };
@@ -0,0 +1,25 @@
1
+ //#region src/externalizedTasks.ts
2
+ var e = 6e4, t = (e) => {
3
+ if (!Number.isFinite(e) || e < 6e4 || e % 6e4 != 0) return;
4
+ let t = e / 6e4;
5
+ if (t === 1) return "* * * * *";
6
+ if (t < 60) return 60 % t == 0 ? `*/${t} * * * *` : void 0;
7
+ if (t === 60) return "0 * * * *";
8
+ if (t % 60 != 0) return;
9
+ let n = t / 60;
10
+ return n < 24 ? 24 % n == 0 ? `0 */${n} * * *` : void 0 : n === 24 ? "0 0 * * *" : void 0;
11
+ }, n = (e, n) => {
12
+ if (n < 6e4) return `"${e}" ticks every ${n} ms. No platform scheduler can express sub-minute cadence — and this is not a cron: its real trigger is an arrival, and the interval is only the ceiling for when reactivity is unavailable. It stays on its in-process timer.`;
13
+ if (t(n) === void 0) return `"${e}" ticks every ${n} ms, which no five-field cron expression fires at — cron matches instants rather than measuring intervals, so only cadences that tile the hour or the day evenly can be expressed. Choose an interval that divides 60 minutes or 24 hours. It stays on its in-process timer.`;
14
+ }, r = Symbol.for("@voltro/externalizedCoordinatedTasks"), i = () => {
15
+ let e = globalThis, t = e[r];
16
+ if (t !== void 0) return t;
17
+ let n = /* @__PURE__ */ new Map();
18
+ return e[r] = n, n;
19
+ }, a = (e) => {
20
+ i().set(e.name, e);
21
+ }, o = (e) => {
22
+ i().delete(e);
23
+ }, s = (e) => i().get(e), c = () => [...i().keys()].sort();
24
+ //#endregion
25
+ export { e as MIN_EXTERNALIZABLE_INTERVAL_MS, t as cronForInterval, n as externalizationRefusal, s as externalizedTask, c as externalizedTaskNames, a as registerExternalizedTask, o as unregisterExternalizedTask };