@tungthedev/streams-server 0.2.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 (183) hide show
  1. package/CODE_OF_CONDUCT.md +45 -0
  2. package/CONTRIBUTING.md +76 -0
  3. package/LICENSE +201 -0
  4. package/README.md +58 -0
  5. package/SECURITY.md +42 -0
  6. package/bin/prisma-streams-server +2 -0
  7. package/package.json +46 -0
  8. package/src/app.ts +583 -0
  9. package/src/app_core.ts +3144 -0
  10. package/src/app_local.ts +206 -0
  11. package/src/auth.ts +124 -0
  12. package/src/auto_tune.ts +69 -0
  13. package/src/backpressure.ts +66 -0
  14. package/src/bootstrap.ts +613 -0
  15. package/src/compute/demo_entry.ts +415 -0
  16. package/src/compute/demo_site.ts +1242 -0
  17. package/src/compute/entry.ts +19 -0
  18. package/src/compute/package_entry.ts +4 -0
  19. package/src/compute/virtual-modules.d.ts +15 -0
  20. package/src/compute/worker_module_url.ts +9 -0
  21. package/src/concurrency_gate.ts +108 -0
  22. package/src/config.ts +402 -0
  23. package/src/db/bootstrap_store.ts +9 -0
  24. package/src/db/db.ts +2424 -0
  25. package/src/db/schema.ts +925 -0
  26. package/src/db/sqlite_manifest_snapshot.ts +81 -0
  27. package/src/db/sqlite_touch_store.ts +491 -0
  28. package/src/db/sqlite_wal_store.ts +472 -0
  29. package/src/details/full_mode_details.ts +568 -0
  30. package/src/expiry_sweeper.ts +47 -0
  31. package/src/foreground_activity.ts +55 -0
  32. package/src/hist.ts +169 -0
  33. package/src/index/binary_fuse.ts +379 -0
  34. package/src/index/indexer.ts +947 -0
  35. package/src/index/lexicon_file_cache.ts +261 -0
  36. package/src/index/lexicon_format.ts +93 -0
  37. package/src/index/lexicon_indexer.ts +863 -0
  38. package/src/index/run_cache.ts +84 -0
  39. package/src/index/run_format.ts +213 -0
  40. package/src/index/schedule.ts +28 -0
  41. package/src/index/secondary_indexer.ts +901 -0
  42. package/src/index/secondary_schema.ts +105 -0
  43. package/src/ingest.ts +309 -0
  44. package/src/lens/lens.ts +501 -0
  45. package/src/manifest.ts +249 -0
  46. package/src/memory.ts +334 -0
  47. package/src/metrics.ts +147 -0
  48. package/src/metrics_emitter.ts +83 -0
  49. package/src/notifier.ts +180 -0
  50. package/src/objectstore/accounting.ts +151 -0
  51. package/src/objectstore/interface.ts +13 -0
  52. package/src/objectstore/mock_r2.ts +269 -0
  53. package/src/objectstore/null.ts +32 -0
  54. package/src/objectstore/r2.ts +318 -0
  55. package/src/observe/pairing.ts +61 -0
  56. package/src/observe/request.ts +772 -0
  57. package/src/offset.ts +70 -0
  58. package/src/postgres/bootstrap.ts +269 -0
  59. package/src/postgres/companions.ts +197 -0
  60. package/src/postgres/control_restore.ts +109 -0
  61. package/src/postgres/details.ts +189 -0
  62. package/src/postgres/lexicon_index.ts +260 -0
  63. package/src/postgres/routing_index.ts +189 -0
  64. package/src/postgres/rows.ts +132 -0
  65. package/src/postgres/schema.ts +355 -0
  66. package/src/postgres/secondary_index.ts +238 -0
  67. package/src/postgres/segments.ts +900 -0
  68. package/src/postgres/stats.ts +103 -0
  69. package/src/postgres/store.ts +947 -0
  70. package/src/postgres/touch.ts +591 -0
  71. package/src/postgres/types.ts +32 -0
  72. package/src/profiles/evlog/schema.ts +234 -0
  73. package/src/profiles/evlog.ts +473 -0
  74. package/src/profiles/generic.ts +51 -0
  75. package/src/profiles/index.ts +237 -0
  76. package/src/profiles/metrics/block_format.ts +109 -0
  77. package/src/profiles/metrics/normalize.ts +366 -0
  78. package/src/profiles/metrics/schema.ts +319 -0
  79. package/src/profiles/metrics.ts +83 -0
  80. package/src/profiles/otelTraces/normalize.ts +955 -0
  81. package/src/profiles/otelTraces/otlp.ts +1002 -0
  82. package/src/profiles/otelTraces/schema.ts +408 -0
  83. package/src/profiles/otelTraces.ts +390 -0
  84. package/src/profiles/profile.ts +284 -0
  85. package/src/profiles/stateProtocol/change_event_conformance.typecheck.ts +35 -0
  86. package/src/profiles/stateProtocol/changes.ts +24 -0
  87. package/src/profiles/stateProtocol/ingest.ts +115 -0
  88. package/src/profiles/stateProtocol/routes.ts +511 -0
  89. package/src/profiles/stateProtocol/types.ts +6 -0
  90. package/src/profiles/stateProtocol/validation.ts +51 -0
  91. package/src/profiles/stateProtocol.ts +107 -0
  92. package/src/read_filter.ts +468 -0
  93. package/src/reader.ts +2986 -0
  94. package/src/runtime/hash.ts +156 -0
  95. package/src/runtime/hash_vendor/LICENSE.hash-wasm +38 -0
  96. package/src/runtime/hash_vendor/NOTICE.md +8 -0
  97. package/src/runtime/hash_vendor/xxhash3.umd.min.cjs +7 -0
  98. package/src/runtime/hash_vendor/xxhash32.umd.min.cjs +7 -0
  99. package/src/runtime/hash_vendor/xxhash64.umd.min.cjs +7 -0
  100. package/src/runtime/host_runtime.ts +5 -0
  101. package/src/runtime_memory.ts +200 -0
  102. package/src/runtime_memory_sampler.ts +237 -0
  103. package/src/schema/lens_schema.ts +290 -0
  104. package/src/schema/proof.ts +547 -0
  105. package/src/schema/read_json.ts +51 -0
  106. package/src/schema/registry.ts +966 -0
  107. package/src/search/agg_format.ts +638 -0
  108. package/src/search/aggregate.ts +409 -0
  109. package/src/search/binary/codec.ts +162 -0
  110. package/src/search/binary/docset.ts +67 -0
  111. package/src/search/binary/restart_strings.ts +181 -0
  112. package/src/search/binary/varint.ts +34 -0
  113. package/src/search/bitset.ts +19 -0
  114. package/src/search/col_format.ts +382 -0
  115. package/src/search/col_runtime.ts +59 -0
  116. package/src/search/column_encoding.ts +43 -0
  117. package/src/search/companion_file_cache.ts +319 -0
  118. package/src/search/companion_format.ts +327 -0
  119. package/src/search/companion_manager.ts +1305 -0
  120. package/src/search/companion_plan.ts +229 -0
  121. package/src/search/exact_format.ts +281 -0
  122. package/src/search/exact_runtime.ts +55 -0
  123. package/src/search/fts_format.ts +423 -0
  124. package/src/search/fts_runtime.ts +333 -0
  125. package/src/search/query.ts +875 -0
  126. package/src/search/schema.ts +245 -0
  127. package/src/segment/cache.ts +270 -0
  128. package/src/segment/cached_segment.ts +89 -0
  129. package/src/segment/format.ts +403 -0
  130. package/src/segment/segmenter.ts +412 -0
  131. package/src/segment/segmenter_worker.ts +72 -0
  132. package/src/segment/segmenter_workers.ts +130 -0
  133. package/src/server.ts +264 -0
  134. package/src/server_auto_tune.ts +158 -0
  135. package/src/sqlite/adapter.ts +335 -0
  136. package/src/sqlite/runtime_stats.ts +163 -0
  137. package/src/stats.ts +205 -0
  138. package/src/store/append.ts +50 -0
  139. package/src/store/bootstrap_restore_store.ts +71 -0
  140. package/src/store/capabilities.ts +86 -0
  141. package/src/store/full_mode_details_store.ts +71 -0
  142. package/src/store/index_store.ts +104 -0
  143. package/src/store/profile_touch_store.ts +1 -0
  144. package/src/store/rows.ts +144 -0
  145. package/src/store/schema_profile_store.ts +73 -0
  146. package/src/store/schema_publication.ts +6 -0
  147. package/src/store/segment_manifest_store.ts +129 -0
  148. package/src/store/segment_read_store.ts +22 -0
  149. package/src/store/stats_accounting_store.ts +83 -0
  150. package/src/store/touch_store.ts +98 -0
  151. package/src/store/wal_store.ts +21 -0
  152. package/src/stream_size_reconciler.ts +100 -0
  153. package/src/touch/canonical_change.ts +7 -0
  154. package/src/touch/live_keys.ts +158 -0
  155. package/src/touch/live_metrics.ts +841 -0
  156. package/src/touch/live_templates.ts +449 -0
  157. package/src/touch/manager.ts +1292 -0
  158. package/src/touch/process_batch.ts +576 -0
  159. package/src/touch/processor_worker.ts +85 -0
  160. package/src/touch/spec.ts +459 -0
  161. package/src/touch/touch_journal.ts +771 -0
  162. package/src/touch/touch_key_id.ts +20 -0
  163. package/src/touch/worker_pool.ts +191 -0
  164. package/src/touch/worker_protocol.ts +57 -0
  165. package/src/types/proper-lockfile.d.ts +1 -0
  166. package/src/uploader.ts +358 -0
  167. package/src/util/base32_crockford.ts +81 -0
  168. package/src/util/bloom256.ts +67 -0
  169. package/src/util/byte_lru.ts +73 -0
  170. package/src/util/cleanup.ts +22 -0
  171. package/src/util/crc32c.ts +29 -0
  172. package/src/util/ds_error.ts +15 -0
  173. package/src/util/duration.ts +17 -0
  174. package/src/util/endian.ts +53 -0
  175. package/src/util/json_pointer.ts +148 -0
  176. package/src/util/log.ts +25 -0
  177. package/src/util/lru.ts +53 -0
  178. package/src/util/retry.ts +35 -0
  179. package/src/util/siphash.ts +71 -0
  180. package/src/util/stream_paths.ts +50 -0
  181. package/src/util/time.ts +14 -0
  182. package/src/util/yield.ts +3 -0
  183. package/src/util/zstd.ts +24 -0
@@ -0,0 +1,237 @@
1
+ import { mkdirSync, createWriteStream, type WriteStream } from "node:fs";
2
+ import { dirname, extname } from "node:path";
3
+ import { isMainThread, threadId } from "node:worker_threads";
4
+ import { readLinuxStatusRssBreakdown } from "./runtime_memory";
5
+
6
+ type BunJscModule = {
7
+ heapStats?: () => unknown;
8
+ memoryUsage?: () => unknown;
9
+ };
10
+
11
+ type PhaseMeta = Record<string, unknown>;
12
+
13
+ type ActivePhase = {
14
+ key: number;
15
+ label: string;
16
+ startedAtMs: number;
17
+ meta: PhaseMeta;
18
+ };
19
+
20
+ function isPromiseLike<T>(value: unknown): value is PromiseLike<T> {
21
+ return typeof value === "object" && value != null && "then" in value && typeof (value as { then?: unknown }).then === "function";
22
+ }
23
+
24
+ function insertScopeSuffix(path: string, scope: string): string {
25
+ if (scope === "main") return path;
26
+ const extension = extname(path);
27
+ if (extension === "") return `${path}.${scope}`;
28
+ return `${path.slice(0, -extension.length)}.${scope}${extension}`;
29
+ }
30
+
31
+ export class RuntimeMemorySampler {
32
+ private readonly outputPath: string;
33
+ private readonly intervalMs: number;
34
+ private readonly scope: string;
35
+ private stream: WriteStream | null = null;
36
+ private timer: any | null = null;
37
+ private activePhases = new Map<number, ActivePhase>();
38
+ private nextPhaseKey = 0;
39
+ private jscModule: BunJscModule | null | undefined;
40
+ private sampling = false;
41
+ private subsystemProvider: (() => unknown) | null = null;
42
+
43
+ constructor(path: string, opts: { intervalMs?: number; scope?: string } = {}) {
44
+ this.scope = opts.scope?.trim() || "main";
45
+ this.outputPath = insertScopeSuffix(path, this.scope);
46
+ this.intervalMs = Math.max(100, opts.intervalMs ?? 1_000);
47
+ }
48
+
49
+ start(): void {
50
+ if (this.stream) return;
51
+ mkdirSync(dirname(this.outputPath), { recursive: true });
52
+ this.stream = createWriteStream(this.outputPath, { flags: "a" });
53
+ this.writeLine({
54
+ ts: new Date().toISOString(),
55
+ kind: "sampler_start",
56
+ scope: this.scope,
57
+ pid: process.pid,
58
+ thread_id: threadId,
59
+ is_main_thread: isMainThread,
60
+ });
61
+ void this.sample("startup");
62
+ this.timer = setInterval(() => {
63
+ void this.sample("interval");
64
+ }, this.intervalMs);
65
+ }
66
+
67
+ stop(): void {
68
+ if (this.timer) clearInterval(this.timer);
69
+ this.timer = null;
70
+ void this.sample("shutdown");
71
+ this.writeLine({
72
+ ts: new Date().toISOString(),
73
+ kind: "sampler_stop",
74
+ scope: this.scope,
75
+ pid: process.pid,
76
+ thread_id: threadId,
77
+ is_main_thread: isMainThread,
78
+ });
79
+ this.stream?.end();
80
+ this.stream = null;
81
+ }
82
+
83
+ enter(label: string, meta: PhaseMeta = {}): () => void {
84
+ if (!this.stream) return () => {};
85
+ const key = ++this.nextPhaseKey;
86
+ const phase: ActivePhase = {
87
+ key,
88
+ label,
89
+ startedAtMs: Date.now(),
90
+ meta,
91
+ };
92
+ this.activePhases.set(key, phase);
93
+ this.writeLine({
94
+ ts: new Date().toISOString(),
95
+ kind: "phase_enter",
96
+ scope: this.scope,
97
+ pid: process.pid,
98
+ thread_id: threadId,
99
+ label,
100
+ meta,
101
+ active_phase_count: this.activePhases.size,
102
+ });
103
+ void this.sample("phase_enter", { label, meta });
104
+ return () => {
105
+ const current = this.activePhases.get(key);
106
+ if (!current) return;
107
+ this.activePhases.delete(key);
108
+ this.writeLine({
109
+ ts: new Date().toISOString(),
110
+ kind: "phase_exit",
111
+ scope: this.scope,
112
+ pid: process.pid,
113
+ thread_id: threadId,
114
+ label: current.label,
115
+ meta: current.meta,
116
+ duration_ms: Date.now() - current.startedAtMs,
117
+ active_phase_count: this.activePhases.size,
118
+ });
119
+ void this.sample("phase_exit", { label: current.label, meta: current.meta });
120
+ };
121
+ }
122
+
123
+ capture(reason: string, data: PhaseMeta = {}): void {
124
+ if (!this.stream) return;
125
+ this.writeLine({
126
+ ts: new Date().toISOString(),
127
+ kind: "marker",
128
+ scope: this.scope,
129
+ pid: process.pid,
130
+ thread_id: threadId,
131
+ reason,
132
+ data,
133
+ });
134
+ void this.sample(reason, data);
135
+ }
136
+
137
+ setSubsystemProvider(provider: (() => unknown) | null): void {
138
+ this.subsystemProvider = provider;
139
+ }
140
+
141
+ track<T>(label: string, meta: PhaseMeta, fn: () => T): T {
142
+ const leave = this.enter(label, meta);
143
+ try {
144
+ const result = fn();
145
+ if (isPromiseLike(result)) {
146
+ return (Promise.resolve(result).finally(() => leave()) as unknown) as T;
147
+ }
148
+ leave();
149
+ return result;
150
+ } catch (error) {
151
+ leave();
152
+ throw error;
153
+ }
154
+ }
155
+
156
+ private async sample(reason: string, data: PhaseMeta = {}): Promise<void> {
157
+ if (!this.stream || this.sampling) return;
158
+ this.sampling = true;
159
+ try {
160
+ const activePhases = Array.from(this.activePhases.values()).map((phase) => ({
161
+ key: phase.key,
162
+ label: phase.label,
163
+ started_at: new Date(phase.startedAtMs).toISOString(),
164
+ duration_ms: Date.now() - phase.startedAtMs,
165
+ meta: phase.meta,
166
+ }));
167
+ const primary = activePhases.length > 0 ? activePhases[activePhases.length - 1]! : null;
168
+ this.writeLine({
169
+ ts: new Date().toISOString(),
170
+ kind: "sample",
171
+ scope: this.scope,
172
+ pid: process.pid,
173
+ thread_id: threadId,
174
+ is_main_thread: isMainThread,
175
+ reason,
176
+ data,
177
+ process_memory_usage: process.memoryUsage(),
178
+ linux_status_rss: readLinuxStatusRssBreakdown(0),
179
+ jsc_heap_stats: await this.readJscHeapStats(),
180
+ jsc_memory_usage: await this.readJscMemoryUsage(),
181
+ memory_subsystems: this.readSubsystems(),
182
+ active_phases: activePhases,
183
+ primary_phase: primary ? { label: primary.label, duration_ms: primary.duration_ms, meta: primary.meta } : null,
184
+ });
185
+ } finally {
186
+ this.sampling = false;
187
+ }
188
+ }
189
+
190
+ private async readJscHeapStats(): Promise<unknown> {
191
+ const jsc = await this.loadJscModule();
192
+ if (!jsc || typeof jsc.heapStats !== "function") return null;
193
+ try {
194
+ return jsc.heapStats();
195
+ } catch {
196
+ return null;
197
+ }
198
+ }
199
+
200
+ private async readJscMemoryUsage(): Promise<unknown> {
201
+ const jsc = await this.loadJscModule();
202
+ if (!jsc || typeof jsc.memoryUsage !== "function") return null;
203
+ try {
204
+ return jsc.memoryUsage();
205
+ } catch {
206
+ return null;
207
+ }
208
+ }
209
+
210
+ private async loadJscModule(): Promise<BunJscModule | null> {
211
+ if (this.jscModule !== undefined) return this.jscModule;
212
+ if (typeof (globalThis as { Bun?: unknown }).Bun === "undefined") {
213
+ this.jscModule = null;
214
+ return this.jscModule;
215
+ }
216
+ try {
217
+ this.jscModule = (await import("bun:jsc")) as BunJscModule;
218
+ } catch {
219
+ this.jscModule = null;
220
+ }
221
+ return this.jscModule;
222
+ }
223
+
224
+ private writeLine(payload: Record<string, unknown>): void {
225
+ if (!this.stream) return;
226
+ this.stream.write(`${JSON.stringify(payload)}\n`);
227
+ }
228
+
229
+ private readSubsystems(): unknown {
230
+ if (!this.subsystemProvider) return null;
231
+ try {
232
+ return this.subsystemProvider();
233
+ } catch {
234
+ return null;
235
+ }
236
+ }
237
+ }
@@ -0,0 +1,290 @@
1
+ export const DURABLE_LENS_V1_SCHEMA: any = {
2
+ $schema: "https://json-schema.org/draft/2020-12/schema",
3
+ $id: "https://example.com/schemas/durable-lens-v1.schema.json",
4
+ title: "Durable Stream Lens Spec",
5
+ type: "object",
6
+ additionalProperties: false,
7
+ required: ["apiVersion", "schema", "from", "to", "ops"],
8
+ properties: {
9
+ apiVersion: {
10
+ type: "string",
11
+ const: "durable.lens/v1",
12
+ },
13
+ schema: {
14
+ type: "string",
15
+ minLength: 1,
16
+ description: "Logical stream schema/type name (e.g., 'Task').",
17
+ },
18
+ from: {
19
+ type: "integer",
20
+ minimum: 0,
21
+ description: "Source schema version.",
22
+ },
23
+ to: {
24
+ type: "integer",
25
+ minimum: 0,
26
+ description: "Target schema version.",
27
+ },
28
+ description: {
29
+ type: "string",
30
+ },
31
+ ops: {
32
+ type: "array",
33
+ minItems: 1,
34
+ items: { $ref: "#/$defs/op" },
35
+ },
36
+ },
37
+ $defs: {
38
+ jsonPointer: {
39
+ type: "string",
40
+ description: "RFC 6901 JSON Pointer. Empty string refers to the document root.",
41
+ pattern: "^(?:/(?:[^~/]|~0|~1)*)*$",
42
+ },
43
+ jsonScalar: {
44
+ description: "JSON scalar value.",
45
+ type: ["string", "number", "integer", "boolean", "null"],
46
+ },
47
+ embeddedJsonSchema: {
48
+ description: "An embedded JSON Schema fragment (object or boolean).",
49
+ anyOf: [{ type: "object" }, { type: "boolean" }],
50
+ },
51
+ mapTransform: {
52
+ type: "object",
53
+ additionalProperties: false,
54
+ required: ["map"],
55
+ properties: {
56
+ map: {
57
+ type: "object",
58
+ description: "Mapping table for values. Keys must be strings; values are JSON scalars.",
59
+ additionalProperties: { $ref: "#/$defs/jsonScalar" },
60
+ },
61
+ default: {
62
+ $ref: "#/$defs/jsonScalar",
63
+ description: "Default output value used when input is not found in map.",
64
+ },
65
+ },
66
+ },
67
+ builtinTransform: {
68
+ type: "object",
69
+ additionalProperties: false,
70
+ required: ["builtin"],
71
+ properties: {
72
+ builtin: {
73
+ type: "string",
74
+ minLength: 1,
75
+ description: "Name of a built-in, version-stable transform implemented by the system.",
76
+ },
77
+ },
78
+ },
79
+ convertTransform: {
80
+ description: "Restricted conversion: either a total mapping table (+ optional default) or a built-in transform.",
81
+ oneOf: [{ $ref: "#/$defs/mapTransform" }, { $ref: "#/$defs/builtinTransform" }],
82
+ },
83
+ opRename: {
84
+ type: "object",
85
+ additionalProperties: false,
86
+ required: ["op", "from", "to"],
87
+ properties: {
88
+ op: { const: "rename" },
89
+ from: { $ref: "#/$defs/jsonPointer" },
90
+ to: { $ref: "#/$defs/jsonPointer" },
91
+ },
92
+ },
93
+ opCopy: {
94
+ type: "object",
95
+ additionalProperties: false,
96
+ required: ["op", "from", "to"],
97
+ properties: {
98
+ op: { const: "copy" },
99
+ from: { $ref: "#/$defs/jsonPointer" },
100
+ to: { $ref: "#/$defs/jsonPointer" },
101
+ },
102
+ },
103
+ opAdd: {
104
+ type: "object",
105
+ additionalProperties: false,
106
+ required: ["op", "path", "schema"],
107
+ properties: {
108
+ op: { const: "add" },
109
+ path: { $ref: "#/$defs/jsonPointer" },
110
+ schema: { $ref: "#/$defs/embeddedJsonSchema" },
111
+ default: {
112
+ description: "Default value to insert if the field is missing. If omitted, the runtime may derive a default when possible.",
113
+ type: ["object", "array", "string", "number", "integer", "boolean", "null"],
114
+ },
115
+ },
116
+ },
117
+ opRemove: {
118
+ type: "object",
119
+ additionalProperties: false,
120
+ required: ["op", "path", "schema"],
121
+ properties: {
122
+ op: { const: "remove" },
123
+ path: { $ref: "#/$defs/jsonPointer" },
124
+ schema: {
125
+ $ref: "#/$defs/embeddedJsonSchema",
126
+ description: "Schema of the removed field (required so the transformation remains declarative and reversible/validatable).",
127
+ },
128
+ default: {
129
+ description: "Default to use when reconstructing the field (e.g., during backward reasoning/validation).",
130
+ type: ["object", "array", "string", "number", "integer", "boolean", "null"],
131
+ },
132
+ },
133
+ },
134
+ opHoist: {
135
+ type: "object",
136
+ additionalProperties: false,
137
+ required: ["op", "host", "name", "to"],
138
+ properties: {
139
+ op: { const: "hoist" },
140
+ host: {
141
+ $ref: "#/$defs/jsonPointer",
142
+ description: "Pointer to an object field that contains the nested value.",
143
+ },
144
+ name: {
145
+ type: "string",
146
+ minLength: 1,
147
+ description: "Field name inside host to move outward.",
148
+ },
149
+ to: {
150
+ $ref: "#/$defs/jsonPointer",
151
+ description: "Destination pointer for the hoisted value.",
152
+ },
153
+ removeFromHost: {
154
+ type: "boolean",
155
+ default: true,
156
+ description: "If true, remove the nested field from the host after hoisting.",
157
+ },
158
+ },
159
+ },
160
+ opPlunge: {
161
+ type: "object",
162
+ additionalProperties: false,
163
+ required: ["op", "from", "host", "name"],
164
+ properties: {
165
+ op: { const: "plunge" },
166
+ from: {
167
+ $ref: "#/$defs/jsonPointer",
168
+ description: "Pointer to the source field to move inward.",
169
+ },
170
+ host: {
171
+ $ref: "#/$defs/jsonPointer",
172
+ description: "Pointer to the destination object field.",
173
+ },
174
+ name: {
175
+ type: "string",
176
+ minLength: 1,
177
+ description: "Field name inside host to receive the value.",
178
+ },
179
+ createHost: {
180
+ type: "boolean",
181
+ default: true,
182
+ description: "If true, create the destination host object if missing.",
183
+ },
184
+ removeFromSource: {
185
+ type: "boolean",
186
+ default: true,
187
+ description: "If true, remove the source field after plunging.",
188
+ },
189
+ },
190
+ },
191
+ opWrap: {
192
+ type: "object",
193
+ additionalProperties: false,
194
+ required: ["op", "path", "mode"],
195
+ properties: {
196
+ op: { const: "wrap" },
197
+ path: { $ref: "#/$defs/jsonPointer" },
198
+ mode: {
199
+ type: "string",
200
+ enum: ["singleton"],
201
+ description: "singleton: x -> [x]",
202
+ },
203
+ reverseMode: {
204
+ type: "string",
205
+ enum: ["first"],
206
+ default: "first",
207
+ description: "When reversing array->scalar, choose 'first'.",
208
+ },
209
+ },
210
+ },
211
+ opHead: {
212
+ type: "object",
213
+ additionalProperties: false,
214
+ required: ["op", "path"],
215
+ properties: {
216
+ op: { const: "head" },
217
+ path: { $ref: "#/$defs/jsonPointer" },
218
+ reverseMode: {
219
+ type: "string",
220
+ enum: ["singleton"],
221
+ default: "singleton",
222
+ description: "When reversing scalar->array, wrap as [scalar].",
223
+ },
224
+ },
225
+ },
226
+ opConvert: {
227
+ type: "object",
228
+ additionalProperties: false,
229
+ required: ["op", "path", "fromType", "toType", "forward", "backward"],
230
+ properties: {
231
+ op: { const: "convert" },
232
+ path: { $ref: "#/$defs/jsonPointer" },
233
+ fromType: {
234
+ type: "string",
235
+ enum: ["string", "number", "integer", "boolean", "null", "object", "array"],
236
+ },
237
+ toType: {
238
+ type: "string",
239
+ enum: ["string", "number", "integer", "boolean", "null", "object", "array"],
240
+ },
241
+ forward: { $ref: "#/$defs/convertTransform" },
242
+ backward: { $ref: "#/$defs/convertTransform" },
243
+ },
244
+ },
245
+ opIn: {
246
+ type: "object",
247
+ additionalProperties: false,
248
+ required: ["op", "path", "ops"],
249
+ properties: {
250
+ op: { const: "in" },
251
+ path: { $ref: "#/$defs/jsonPointer" },
252
+ ops: {
253
+ type: "array",
254
+ minItems: 1,
255
+ items: { $ref: "#/$defs/op" },
256
+ },
257
+ },
258
+ },
259
+ opMap: {
260
+ type: "object",
261
+ additionalProperties: false,
262
+ required: ["op", "path", "ops"],
263
+ properties: {
264
+ op: { const: "map" },
265
+ path: { $ref: "#/$defs/jsonPointer" },
266
+ ops: {
267
+ type: "array",
268
+ minItems: 1,
269
+ items: { $ref: "#/$defs/op" },
270
+ },
271
+ },
272
+ },
273
+ op: {
274
+ description: "A single lens operation.",
275
+ oneOf: [
276
+ { $ref: "#/$defs/opRename" },
277
+ { $ref: "#/$defs/opCopy" },
278
+ { $ref: "#/$defs/opAdd" },
279
+ { $ref: "#/$defs/opRemove" },
280
+ { $ref: "#/$defs/opHoist" },
281
+ { $ref: "#/$defs/opPlunge" },
282
+ { $ref: "#/$defs/opWrap" },
283
+ { $ref: "#/$defs/opHead" },
284
+ { $ref: "#/$defs/opConvert" },
285
+ { $ref: "#/$defs/opIn" },
286
+ { $ref: "#/$defs/opMap" },
287
+ ],
288
+ },
289
+ },
290
+ };