@nyte-ai/host 0.0.0-stage → 0.0.18

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 (235) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +6 -2
  3. package/THIRD-PARTY-NOTICES.md +800 -0
  4. package/dist/chunks/canvas-preview-0cmbkp2j.js +65 -0
  5. package/dist/chunks/canvas-preview-0nq75h9k.js +7 -0
  6. package/dist/chunks/canvas-preview-0nrnbt76.js +497 -0
  7. package/dist/chunks/canvas-preview-1sq7z9jq.js +17 -0
  8. package/dist/chunks/canvas-preview-1ta3avah.js +56 -0
  9. package/dist/chunks/canvas-preview-2zdyh9ny.js +26 -0
  10. package/dist/chunks/canvas-preview-31w7xm8z.js +34 -0
  11. package/dist/chunks/canvas-preview-4b6sayhg.js +295 -0
  12. package/dist/chunks/canvas-preview-4e0pxa32.js +63 -0
  13. package/dist/chunks/canvas-preview-518dh8by.js +169 -0
  14. package/dist/chunks/canvas-preview-5hxat7hh.js +239 -0
  15. package/dist/chunks/canvas-preview-5mex725c.js +14 -0
  16. package/dist/chunks/canvas-preview-5z63am63.js +295 -0
  17. package/dist/chunks/canvas-preview-65xqjw7f.js +20 -0
  18. package/dist/chunks/canvas-preview-69fkk9yn.js +95 -0
  19. package/dist/chunks/canvas-preview-75db60xy.js +457 -0
  20. package/dist/chunks/canvas-preview-76fwvd6e.js +272 -0
  21. package/dist/chunks/canvas-preview-8xgt67ka.js +598 -0
  22. package/dist/chunks/canvas-preview-98aj5p34.js +176 -0
  23. package/dist/chunks/canvas-preview-9aqczacs.js +73 -0
  24. package/dist/chunks/canvas-preview-a3yw0j70.js +72 -0
  25. package/dist/chunks/canvas-preview-azg56359.js +68 -0
  26. package/dist/chunks/canvas-preview-d9wwhbtz.js +240 -0
  27. package/dist/chunks/canvas-preview-ehf3vp31.js +411 -0
  28. package/dist/chunks/canvas-preview-evh92ywd.js +369 -0
  29. package/dist/chunks/canvas-preview-ewgspmsd.js +81 -0
  30. package/dist/chunks/canvas-preview-g3qj3y3k.js +156 -0
  31. package/dist/chunks/canvas-preview-h8hshnff.js +63 -0
  32. package/dist/chunks/canvas-preview-j1tnf51w.js +36 -0
  33. package/dist/chunks/canvas-preview-kdqgrstc.js +194 -0
  34. package/dist/chunks/canvas-preview-kn7kvbfj.js +12 -0
  35. package/dist/chunks/canvas-preview-m02v4yy6.js +386 -0
  36. package/dist/chunks/canvas-preview-mgcx2b2p.js +271 -0
  37. package/dist/chunks/canvas-preview-rp4x6y1h.js +144 -0
  38. package/dist/chunks/canvas-preview-rp6fz0w5.js +130 -0
  39. package/dist/chunks/canvas-preview-s94ebwf5.js +112 -0
  40. package/dist/chunks/canvas-preview-senpjwep.js +518 -0
  41. package/dist/chunks/canvas-preview-skzwvw4d.js +23 -0
  42. package/dist/chunks/canvas-preview-srgfpdmf.js +60 -0
  43. package/dist/chunks/canvas-preview-tjhabynd.js +252 -0
  44. package/dist/chunks/canvas-preview-vaatfznm.js +189 -0
  45. package/dist/chunks/canvas-preview-vax9ne4e.js +341 -0
  46. package/dist/chunks/canvas-preview-wg80rbc2.js +46 -0
  47. package/dist/chunks/canvas-preview-xn10d4f8.js +608 -0
  48. package/dist/chunks/canvas-preview-yjz0f7n4.js +50 -0
  49. package/dist/chunks/canvas-preview-ymgz0937.js +587 -0
  50. package/dist/chunks/canvas-preview-yrfz2ak6.js +134 -0
  51. package/dist/chunks/canvas-preview-zaaj99wd.js +244 -0
  52. package/dist/chunks/canvas-preview-zn2d0k9d.js +133 -0
  53. package/dist/src/canvas-preview.d.ts +29 -0
  54. package/dist/src/canvas-preview.js +267 -0
  55. package/dist/src/credentials.d.ts +44 -0
  56. package/dist/src/credentials.js +10 -0
  57. package/dist/src/deploy/cloudflare/worker.d.ts +50 -0
  58. package/dist/src/deploy/cloudflare/worker.js +196 -0
  59. package/dist/src/deploy/vercel/nitro.config.d.ts +2 -0
  60. package/dist/src/deploy/vercel/nitro.config.js +30 -0
  61. package/dist/src/deploy/vercel/server/routes/[...].d.ts +2 -0
  62. package/dist/src/deploy/vercel/server/routes/[...].js +18 -0
  63. package/dist/src/deploy/vercel/server/utils/host.d.ts +13 -0
  64. package/dist/src/deploy/vercel/server/utils/host.js +7 -0
  65. package/dist/src/deploy/vercel/server/utils/repair.d.ts +1 -0
  66. package/dist/src/deploy/vercel/server/utils/repair.js +6 -0
  67. package/dist/src/deploy/vercel/server/utils/step.d.ts +10 -0
  68. package/dist/src/deploy/vercel/server/utils/step.js +6 -0
  69. package/dist/src/deploy/vercel/server/workflows/drive.d.ts +14 -0
  70. package/dist/src/deploy/vercel/server/workflows/drive.js +13 -0
  71. package/dist/src/durable-object.d.ts +14 -0
  72. package/dist/src/durable-object.js +231 -0
  73. package/dist/src/hash.d.ts +1 -0
  74. package/dist/src/hash.js +6 -0
  75. package/dist/src/identity-key.d.ts +11 -0
  76. package/dist/src/identity-key.js +9 -0
  77. package/dist/src/identity.d.ts +12 -0
  78. package/dist/src/identity.js +6 -0
  79. package/dist/src/index.d.ts +21 -0
  80. package/dist/src/index.js +120 -0
  81. package/dist/src/key-file.d.ts +1 -0
  82. package/dist/src/key-file.js +7 -0
  83. package/dist/src/listen-fetch.d.ts +8 -0
  84. package/dist/src/listen-fetch.js +9 -0
  85. package/dist/src/listen.d.ts +4 -0
  86. package/dist/src/listen.js +10 -0
  87. package/dist/src/local/command.d.ts +15 -0
  88. package/dist/src/local/command.js +8 -0
  89. package/dist/src/local/feed.d.ts +7 -0
  90. package/dist/src/local/feed.js +6 -0
  91. package/dist/src/local/files.d.ts +18 -0
  92. package/dist/src/local/files.js +13 -0
  93. package/dist/src/local/git-watch.d.ts +15 -0
  94. package/dist/src/local/git-watch.js +7 -0
  95. package/dist/src/local/git.d.ts +5 -0
  96. package/dist/src/local/git.js +13 -0
  97. package/dist/src/local/github.d.ts +30 -0
  98. package/dist/src/local/github.js +9 -0
  99. package/dist/src/local/identity.d.ts +3 -0
  100. package/dist/src/local/identity.js +7 -0
  101. package/dist/src/local/jobs.d.ts +7 -0
  102. package/dist/src/local/jobs.js +6 -0
  103. package/dist/src/local/legacy-trust.d.ts +10 -0
  104. package/dist/src/local/legacy-trust.js +6 -0
  105. package/dist/src/local/patch.d.ts +16 -0
  106. package/dist/src/local/patch.js +17 -0
  107. package/dist/src/local/provider.d.ts +11 -0
  108. package/dist/src/local/provider.js +20 -0
  109. package/dist/src/local/snapshots.d.ts +7 -0
  110. package/dist/src/local/snapshots.js +11 -0
  111. package/dist/src/local/text.d.ts +1 -0
  112. package/dist/src/local/text.js +6 -0
  113. package/dist/src/local/tree.d.ts +50 -0
  114. package/dist/src/local/tree.js +16 -0
  115. package/dist/src/local/workspace-migration.d.ts +2 -0
  116. package/dist/src/local/workspace-migration.js +7 -0
  117. package/dist/src/local/worktrees.d.ts +46 -0
  118. package/dist/src/local/worktrees.js +14 -0
  119. package/dist/src/mcp/config.d.ts +75 -0
  120. package/dist/src/mcp/config.js +12 -0
  121. package/dist/src/mcp/connect.d.ts +28 -0
  122. package/dist/src/mcp/connect.js +9 -0
  123. package/dist/src/mcp/hub.d.ts +19 -0
  124. package/dist/src/mcp/hub.js +11 -0
  125. package/dist/src/mcp/oauth.d.ts +46 -0
  126. package/dist/src/mcp/oauth.js +12 -0
  127. package/dist/src/mcp/tools.d.ts +29 -0
  128. package/dist/src/mcp/tools.js +12 -0
  129. package/dist/src/open.d.ts +42 -0
  130. package/dist/src/open.js +44 -0
  131. package/dist/src/plugins/loader.d.ts +21 -0
  132. package/dist/src/plugins/loader.js +11 -0
  133. package/dist/src/postgres.d.ts +19 -0
  134. package/dist/src/postgres.js +8 -0
  135. package/dist/src/providers.d.ts +16 -0
  136. package/dist/src/providers.js +112 -0
  137. package/dist/src/secrets.d.ts +12 -0
  138. package/dist/src/secrets.js +8 -0
  139. package/dist/src/serve/binding.d.ts +39 -0
  140. package/dist/src/serve/binding.js +6 -0
  141. package/dist/src/serve/handler.d.ts +15 -0
  142. package/dist/src/serve/handler.js +8 -0
  143. package/dist/src/serve/stream-server.d.ts +6 -0
  144. package/dist/src/serve/stream-server.js +6 -0
  145. package/dist/src/session-index.d.ts +11 -0
  146. package/dist/src/session-index.js +11 -0
  147. package/dist/src/shell-env.d.ts +14 -0
  148. package/dist/src/shell-env.js +108 -0
  149. package/dist/src/sqlite-workers.d.ts +6 -0
  150. package/dist/src/sqlite-workers.js +6 -0
  151. package/dist/src/sqlite.d.ts +7 -0
  152. package/dist/src/sqlite.js +10 -0
  153. package/dist/src/store-owner.d.ts +15 -0
  154. package/dist/src/store-owner.js +9 -0
  155. package/dist/src/store-rows.d.ts +13 -0
  156. package/dist/src/store-rows.js +26 -0
  157. package/dist/src/store-usage.d.ts +25 -0
  158. package/dist/src/store-usage.js +16 -0
  159. package/dist/src/usage-scan.d.ts +24 -0
  160. package/dist/src/usage-scan.js +65 -0
  161. package/dist/src/usage-service.d.ts +22 -0
  162. package/dist/src/usage-service.js +12 -0
  163. package/dist/src/usage-summary.d.ts +9 -0
  164. package/dist/src/usage-summary.js +16 -0
  165. package/dist/src/usage-types.d.ts +86 -0
  166. package/dist/src/usage-types.js +0 -0
  167. package/dist/src/usage-worker.d.ts +1 -0
  168. package/dist/src/usage-worker.js +25 -0
  169. package/dist/src/usage.d.ts +72 -0
  170. package/dist/src/usage.js +13 -0
  171. package/package.json +132 -3
  172. package/src/canvas-preview.ts +402 -0
  173. package/src/credentials.ts +294 -0
  174. package/src/deploy/cloudflare/README.md +3 -0
  175. package/src/deploy/cloudflare/worker.ts +306 -0
  176. package/src/deploy/cloudflare/wrangler.jsonc +12 -0
  177. package/src/deploy/vercel/README.md +3 -0
  178. package/src/deploy/vercel/nitro.config.ts +29 -0
  179. package/src/deploy/vercel/server/routes/[...].ts +10 -0
  180. package/src/deploy/vercel/server/utils/host.ts +104 -0
  181. package/src/deploy/vercel/server/utils/repair.ts +35 -0
  182. package/src/deploy/vercel/server/utils/step.ts +25 -0
  183. package/src/deploy/vercel/server/workflows/drive.ts +46 -0
  184. package/src/deploy/vercel/tsconfig.json +9 -0
  185. package/src/deploy/vercel/types/bun.d.ts +2 -0
  186. package/src/durable-object.ts +386 -0
  187. package/src/hash.ts +5 -0
  188. package/src/identity-key.ts +20 -0
  189. package/src/identity.ts +66 -0
  190. package/src/index.ts +21 -0
  191. package/src/key-file.ts +53 -0
  192. package/src/listen-fetch.ts +409 -0
  193. package/src/listen.ts +228 -0
  194. package/src/local/command.ts +109 -0
  195. package/src/local/feed.ts +70 -0
  196. package/src/local/files.ts +812 -0
  197. package/src/local/git-watch.ts +189 -0
  198. package/src/local/git.ts +914 -0
  199. package/src/local/github.ts +692 -0
  200. package/src/local/identity.ts +21 -0
  201. package/src/local/jobs.ts +587 -0
  202. package/src/local/legacy-trust.ts +105 -0
  203. package/src/local/patch.ts +334 -0
  204. package/src/local/provider.ts +184 -0
  205. package/src/local/snapshots.ts +776 -0
  206. package/src/local/text.ts +9 -0
  207. package/src/local/tree.ts +398 -0
  208. package/src/local/workspace-migration.ts +95 -0
  209. package/src/local/worktrees.ts +239 -0
  210. package/src/mcp/config.ts +330 -0
  211. package/src/mcp/connect.ts +105 -0
  212. package/src/mcp/hub.ts +836 -0
  213. package/src/mcp/oauth.ts +436 -0
  214. package/src/mcp/tools.ts +333 -0
  215. package/src/open.ts +469 -0
  216. package/src/plugins/loader.ts +186 -0
  217. package/src/postgres.ts +492 -0
  218. package/src/providers.ts +135 -0
  219. package/src/secrets.ts +31 -0
  220. package/src/serve/binding.ts +628 -0
  221. package/src/serve/handler.ts +362 -0
  222. package/src/serve/stream-server.ts +176 -0
  223. package/src/session-index.ts +99 -0
  224. package/src/shell-env.ts +153 -0
  225. package/src/sqlite-workers.ts +283 -0
  226. package/src/sqlite.ts +528 -0
  227. package/src/store-owner.ts +98 -0
  228. package/src/store-rows.ts +110 -0
  229. package/src/store-usage.ts +344 -0
  230. package/src/usage-scan.ts +81 -0
  231. package/src/usage-service.ts +73 -0
  232. package/src/usage-summary.ts +79 -0
  233. package/src/usage-types.ts +87 -0
  234. package/src/usage-worker.ts +24 -0
  235. package/src/usage.ts +701 -0
package/src/sqlite.ts ADDED
@@ -0,0 +1,528 @@
1
+ import { mkdirSync, readdirSync, rmSync, writeFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { DatabaseSync } from "node:sqlite";
4
+ import type { SQLInputValue, StatementSync } from "node:sqlite";
5
+ import { asSeq } from "@nyte-ai/schema";
6
+ import type { ObjectId, Ref, Revision, Seq, SessionId } from "@nyte-ai/schema";
7
+ import { dev } from "@nyte-ai/core";
8
+ import {
9
+ admits,
10
+ fenceNames,
11
+ assertStoreVersion,
12
+ blobId,
13
+ decodeObject,
14
+ collectSessionEvents,
15
+ migrateStore,
16
+ objectId,
17
+ sessionIndexRef,
18
+ parseSessionRef,
19
+ } from "@nyte-ai/core/store";
20
+ import type { Store, StoreConnection } from "@nyte-ai/core";
21
+ import type { MigrationTarget, Relog } from "@nyte-ai/core/store";
22
+ import {
23
+ collect,
24
+ decodeEvent,
25
+ loggedRow,
26
+ memoryReader,
27
+ mintRevision,
28
+ parseNumber,
29
+ prefixEnd,
30
+ parseRef,
31
+ parseStoredObject,
32
+ parseStoredRecord,
33
+ parseText,
34
+ } from "./store-rows.ts";
35
+ import { sqliteWorkers } from "./sqlite-workers.ts";
36
+ import { sessionIndexChanges, sqlSessionIndex } from "./session-index.ts";
37
+ import { claimStore } from "./store-owner.ts";
38
+
39
+ const BUSY_TIMEOUT_MS = 5_000;
40
+
41
+ const BACKUPS_KEPT = 3;
42
+
43
+ const PUT_BODY =
44
+ "INSERT INTO objects (id, body) VALUES (?, ?) ON CONFLICT (id) DO UPDATE SET body = excluded.body WHERE objects.body IS NULL";
45
+
46
+ /** Returns a `Store` over one `node:sqlite` `DatabaseSync`: a file at `path` (WAL mode, opened only by the process that `claimStore` makes its owner, so a second process rejects with `SdkError("unavailable")` before it reads or migrates the file) or an in-memory database. The first `open()` brings it to core's `STORE_VERSION` with `migrateStore` (core's `objects`, `refs`, `log` and `store_meta` tables; each commit one `BEGIN IMMEDIATE`), a file store whose migration rewrites records first saving those refs' rows (name, target, revision; objects are never deleted, so the rows restore them) to `backups/store-<time>-<pid>-<uuid>-v<from>-v<to>.json` beside it, keeping the newest three backups, and rejects with `SdkError("unavailable")` for a store a newer Nyte migrated; a `migrations` table an older Nyte left is kept and ignored. `put` ids the value with core's `objectId` and stores its JSON once; `putBlob` buffers the stream, ids it with `blobId` and stores the bytes once; an object and a blob that share an id share the row, each column filled once (§41f); `blob` reads the bytes back with their size and a null mime. `refs.set` is one `BEGIN IMMEDIATE` transaction with `RefStore.set`'s semantics, each revision a random UUID, that first rejects with `SdkError("unavailable")` when a newer Nyte has migrated the store since it opened. `list(prefix)` reads the name range from `prefix` in name order (BINARY, by code point); `log` returns the events after `after` in seq order up to `limit`, each validated as a `SessionEvent`; `tail` is the session's last seq, or 0; `commitMark` is the writer connection's `PRAGMA data_version`, which moves only for another connection's commit. `checkpoints` keeps one fold checkpoint per session in `fold_checkpoints`, a write at a lower seq leaving the kept one, and `gap` counts a session's events past a seq and sums their UTF-8 bytes. File stores run `list` and `log` on a read-only worker and WAL checkpoints on a separate worker, checking at 1,000 frames and after 100 ms idle with a 10,000-frame writer backstop; unavailable workers warn and fall back to the main connection. Checkpoints use fullfsync on macOS, WAL reuse retains at most 32 MiB, and opening refreshes planner statistics. Writes binding more than 64 KiB use uncached statements. Every `open()` returns a connection over the one writer; `close()` waits for opening and worker shutdown before closing it, and an open during close waits to reopen. */
47
+ export function sqliteStore(input: { readonly path: string } | { readonly memory: true }): Store {
48
+ let database: DatabaseSync | null = null;
49
+ let opening: Promise<StoreConnection> | null = null;
50
+ let closing: Promise<void> | null = null;
51
+ let workers: ReturnType<typeof sqliteWorkers> | null = null;
52
+ let release: (() => void) | null = null;
53
+
54
+ const open = async (): Promise<StoreConnection> => {
55
+ const claim = "path" in input ? claimStore(input.path) : null;
56
+ let created: DatabaseSync | undefined;
57
+
58
+ try {
59
+ created = new DatabaseSync("path" in input ? input.path : ":memory:", {
60
+ timeout: BUSY_TIMEOUT_MS,
61
+ });
62
+
63
+ if ("path" in input) created.exec("PRAGMA journal_mode = WAL");
64
+ created.exec("PRAGMA synchronous = NORMAL");
65
+ created.exec("PRAGMA checkpoint_fullfsync = ON");
66
+ created.exec("PRAGMA journal_size_limit = 33554432");
67
+
68
+ await migrateStore(sqliteTarget(created, "path" in input ? input.path : null));
69
+ created.exec("PRAGMA optimize = 0x10012");
70
+ } catch (error) {
71
+ created?.close();
72
+ claim?.();
73
+
74
+ throw error;
75
+ }
76
+
77
+ database = created;
78
+ release = claim;
79
+ workers = "path" in input ? sqliteWorkers(created, input.path) : null;
80
+
81
+ return connect(created, workers);
82
+ };
83
+
84
+ const connection = (): Promise<StoreConnection> => {
85
+ if (closing !== null) return closing.then(connection);
86
+ opening ??= open().catch((error: unknown) => {
87
+ opening = null;
88
+
89
+ throw error;
90
+ });
91
+
92
+ return opening;
93
+ };
94
+
95
+ return {
96
+ open: connection,
97
+ close() {
98
+ closing ??= (async () => {
99
+ await opening?.catch(() => undefined);
100
+ await workers?.close();
101
+ database?.close();
102
+ release?.();
103
+ workers = null;
104
+ database = null;
105
+ release = null;
106
+ opening = null;
107
+ })().finally(() => {
108
+ closing = null;
109
+ });
110
+
111
+ return closing;
112
+ },
113
+ };
114
+ }
115
+
116
+ function hasTable(database: DatabaseSync, name: string): boolean {
117
+ return (
118
+ database.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?").get(name) !==
119
+ undefined
120
+ );
121
+ }
122
+
123
+ function storedVersion(database: DatabaseSync): number {
124
+ if (!hasTable(database, "store_meta")) return 0;
125
+ const row = database.prepare("SELECT version FROM store_meta WHERE id = 1").get();
126
+
127
+ return row === undefined ? 0 : parseNumber(row.version);
128
+ }
129
+
130
+ function sqliteTarget(database: DatabaseSync, path: string | null): MigrationTarget {
131
+ const target: MigrationTarget = {
132
+ dialect: "sqlite",
133
+ async stamp() {
134
+ return {
135
+ version: storedVersion(database),
136
+ empty:
137
+ !hasTable(database, "refs") ||
138
+ database.prepare("SELECT 1 FROM refs LIMIT 1").get() === undefined,
139
+ };
140
+ },
141
+ async records() {
142
+ return database
143
+ .prepare(
144
+ "SELECT refs.name, refs.target, refs.revision, objects.body FROM refs JOIN objects ON objects.id = refs.target WHERE objects.body IS NOT NULL",
145
+ )
146
+ .all()
147
+ .map(parseStoredRecord);
148
+ },
149
+ async *objects(kind) {
150
+ yield* database
151
+ .prepare("SELECT id, body FROM objects WHERE body IS NOT NULL AND instr(body, ?) > 0")
152
+ .all(`"kind":${JSON.stringify(kind)}`)
153
+ .map(parseStoredObject);
154
+ },
155
+ async *logs() {
156
+ if (!hasTable(database, "log")) return;
157
+ const read = database.prepare("SELECT seq, event FROM log WHERE session = ? ORDER BY seq");
158
+
159
+ for (const row of database.prepare("SELECT DISTINCT session FROM log").all()) {
160
+ const session = parseText(row.session);
161
+
162
+ if (session !== null) yield { session, events: read.all(session).map(loggedRow) };
163
+ }
164
+ },
165
+ async commit(change) {
166
+ database.exec("BEGIN IMMEDIATE");
167
+
168
+ try {
169
+ if (storedVersion(database) !== change.from) {
170
+ database.exec("ROLLBACK");
171
+
172
+ return "moved";
173
+ }
174
+
175
+ for (const statement of change.sql) database.exec(statement);
176
+ const putBody = database.prepare(PUT_BODY);
177
+
178
+ const repoint = database.prepare(
179
+ "UPDATE refs SET target = ?, revision = ? WHERE name = ? AND revision = ?",
180
+ );
181
+
182
+ for (const moved of change.repoints) {
183
+ write(database, putBody, moved.id, moved.body);
184
+
185
+ if (
186
+ Number(repoint.run(moved.id, mintRevision(), moved.name, moved.expect).changes) === 0
187
+ ) {
188
+ database.exec("ROLLBACK");
189
+
190
+ return "conflict";
191
+ }
192
+ updateSessionIndex(database, moved.name, moved.id, mintRevision());
193
+ }
194
+
195
+ const create = database.prepare(
196
+ "INSERT INTO refs (name, target, revision) VALUES (?, ?, ?) ON CONFLICT (name) DO NOTHING",
197
+ );
198
+
199
+ for (const created of change.creates) {
200
+ write(database, putBody, created.target, created.body);
201
+ const revision = mintRevision();
202
+
203
+ if (Number(create.run(created.name, created.target, revision).changes) > 0)
204
+ updateSessionIndex(database, created.name, created.target, revision);
205
+ }
206
+
207
+ const relog = database.prepare(
208
+ "UPDATE log SET event = ? WHERE session = ? AND seq = ? AND event = ?",
209
+ );
210
+
211
+ for (const row of change.relogs) {
212
+ if (Number(relog.run(row.event, row.session, row.seq, row.expect).changes) === 0) {
213
+ database.exec("ROLLBACK");
214
+
215
+ return "conflict";
216
+ }
217
+ }
218
+
219
+ if (change.reindexSessions)
220
+ for (const row of database.prepare("SELECT name, target, revision FROM refs").all()) {
221
+ const ref = parseRef(row);
222
+ updateSessionIndex(database, ref.name, ref.target, ref.revision);
223
+ }
224
+
225
+ database
226
+ .prepare(
227
+ "INSERT INTO store_meta (id, version) VALUES (1, ?) ON CONFLICT (id) DO UPDATE SET version = excluded.version",
228
+ )
229
+ .run(change.to);
230
+ database.exec("COMMIT");
231
+
232
+ return "committed";
233
+ } catch (error) {
234
+ if (database.isTransaction) database.exec("ROLLBACK");
235
+
236
+ throw error;
237
+ }
238
+ },
239
+ };
240
+
241
+ if (path === null) return target;
242
+
243
+ return {
244
+ ...target,
245
+ backup: async (from, to, names, log) => backup(database, path, from, to, names, log),
246
+ };
247
+ }
248
+
249
+ function backup(
250
+ database: DatabaseSync,
251
+ path: string,
252
+ from: number,
253
+ to: number,
254
+ names: readonly string[],
255
+ log: readonly Relog[],
256
+ ): void {
257
+ const folder = join(dirname(path), "backups");
258
+ mkdirSync(folder, { recursive: true, mode: 0o700 });
259
+ const at = new Date().toISOString().replaceAll(":", "-");
260
+ const read = database.prepare("SELECT name, target, revision FROM refs WHERE name = ?");
261
+
262
+ const rows = names.flatMap((name) => {
263
+ const row = read.get(name);
264
+
265
+ return row === undefined ? [] : [parseRef(row)];
266
+ });
267
+
268
+ writeFileSync(
269
+ join(folder, `store-${at}-${process.pid}-${crypto.randomUUID()}-v${from}-v${to}.json`),
270
+ JSON.stringify({
271
+ refs: rows,
272
+ log: log.map((row) => ({ session: row.session, seq: row.seq, event: row.expect })),
273
+ }),
274
+ { mode: 0o600 },
275
+ );
276
+
277
+ const copies = readdirSync(folder)
278
+ .filter((file) => /^store-.+-v\d+-v\d+\.(?:db|json)$/u.test(file))
279
+ .sort();
280
+
281
+ for (const file of copies.slice(0, -BACKUPS_KEPT)) rmSync(join(folder, file), { force: true });
282
+ }
283
+
284
+ function connect(
285
+ database: DatabaseSync,
286
+ workers: ReturnType<typeof sqliteWorkers> | null,
287
+ ): StoreConnection {
288
+ const putBody = database.prepare(PUT_BODY);
289
+
290
+ const putBytes = database.prepare(
291
+ "INSERT INTO objects (id, bytes) VALUES (?, ?) ON CONFLICT (id) DO UPDATE SET bytes = excluded.bytes WHERE objects.bytes IS NULL",
292
+ );
293
+
294
+ const getBody = database.prepare("SELECT body FROM objects WHERE id = ?");
295
+ const getBytes = database.prepare("SELECT bytes FROM objects WHERE id = ?");
296
+ const getRef = database.prepare("SELECT name, target, revision FROM refs WHERE name = ?");
297
+
298
+ const listFrom = database.prepare(
299
+ "SELECT name, target, revision FROM refs WHERE name >= ? ORDER BY name",
300
+ );
301
+
302
+ const listRange = database.prepare(
303
+ "SELECT name, target, revision FROM refs WHERE name >= ? AND name < ? ORDER BY name",
304
+ );
305
+
306
+ const putRef = database.prepare(
307
+ "INSERT INTO refs (name, target, revision) VALUES (?, ?, ?) ON CONFLICT (name) DO UPDATE SET target = excluded.target, revision = excluded.revision",
308
+ );
309
+
310
+ const deleteRef = database.prepare("DELETE FROM refs WHERE name = ?");
311
+ const append = database.prepare("INSERT INTO log (session, seq, at, event) VALUES (?, ?, ?, ?)");
312
+
313
+ const readLog = database.prepare(
314
+ "SELECT seq, at, event FROM log WHERE session = ? AND seq > ? ORDER BY seq LIMIT ?",
315
+ );
316
+
317
+ const readTail = database.prepare("SELECT MAX(seq) AS tail FROM log WHERE session = ?");
318
+
319
+ const readCheckpoint = database.prepare(
320
+ "SELECT seq, body FROM fold_checkpoints WHERE session = ?",
321
+ );
322
+
323
+ const writeCheckpoint = database.prepare(
324
+ "INSERT INTO fold_checkpoints (session, seq, body) VALUES (?, ?, ?) ON CONFLICT (session) DO UPDATE SET seq = excluded.seq, body = excluded.body WHERE excluded.seq >= fold_checkpoints.seq",
325
+ );
326
+
327
+ const readGap = database.prepare(
328
+ "SELECT COUNT(*) AS events, COALESCE(SUM(LENGTH(CAST(event AS BLOB))), 0) AS bytes FROM log WHERE session = ? AND seq > ?",
329
+ );
330
+ const readVersion = database.prepare("SELECT version FROM store_meta WHERE id = 1");
331
+ const readDataVersion = database.prepare("PRAGMA data_version");
332
+
333
+ const read = async (statement: StatementSync, ...params: SQLInputValue[]) =>
334
+ (await workers?.read(statement.sourceSQL, params)) ?? statement.all(...params);
335
+
336
+ const tail = (session: SessionId): Seq => {
337
+ const row = readTail.get(session);
338
+
339
+ return asSeq(row?.tail === null || row?.tail === undefined ? 0 : parseNumber(row.tail));
340
+ };
341
+
342
+ const ref = (name: string): Ref | null => {
343
+ const row = getRef.get(name);
344
+
345
+ return row === undefined ? null : parseRef(row);
346
+ };
347
+
348
+ return {
349
+ commitMark: async () => parseNumber(readDataVersion.get()?.data_version),
350
+ sessions: sqlSessionIndex(
351
+ async (text, values) =>
352
+ (await workers?.read(text, values)) ?? database.prepare(text).all(...values),
353
+ ),
354
+ objects: {
355
+ async put(object) {
356
+ const id = await objectId(object);
357
+ write(database, putBody, id, JSON.stringify(object));
358
+ workers?.check();
359
+
360
+ return id;
361
+ },
362
+ async get(id) {
363
+ const body = parseText(getBody.get(id)?.body);
364
+
365
+ return body === null ? null : decodeObject(body);
366
+ },
367
+ async putBlob(bytes) {
368
+ const joined = await collect(bytes);
369
+ const id = await blobId(joined);
370
+ write(database, putBytes, id, joined);
371
+ workers?.check();
372
+
373
+ return id;
374
+ },
375
+ async blob(id) {
376
+ const bytes = getBytes.get(id)?.bytes;
377
+
378
+ return bytes instanceof Uint8Array ? memoryReader(bytes) : null;
379
+ },
380
+ },
381
+ refs: {
382
+ get: async (name) => ref(name),
383
+ async list(prefix) {
384
+ const end = prefixEnd(prefix);
385
+
386
+ const rows =
387
+ end === null ? await read(listFrom, prefix) : await read(listRange, prefix, end);
388
+
389
+ return rows.map(parseRef);
390
+ },
391
+ async set(updates) {
392
+ const logged = collectSessionEvents(updates);
393
+
394
+ const waiting = performance.now();
395
+ database.exec("BEGIN IMMEDIATE");
396
+ const acquired = performance.now();
397
+
398
+ try {
399
+ assertStoreVersion(parseNumber(readVersion.get()?.version ?? 0));
400
+ const before = new Map<string, Ref | undefined>();
401
+
402
+ for (const update of updates)
403
+ if (!before.has(update.name)) before.set(update.name, ref(update.name) ?? undefined);
404
+ const fenced = fenceNames(updates).find((name) => ref(name) === null);
405
+
406
+ if (fenced !== undefined) {
407
+ database.exec("ROLLBACK");
408
+
409
+ return { kind: "fenced", name: fenced };
410
+ }
411
+
412
+ const stale = updates.find((update) => !admits(update, before.get(update.name)));
413
+
414
+ if (stale !== undefined) {
415
+ database.exec("ROLLBACK");
416
+
417
+ return {
418
+ kind: "stale",
419
+ name: stale.name,
420
+ revision: before.get(stale.name)?.revision ?? null,
421
+ };
422
+ }
423
+
424
+ const touched = new Map<SessionId, Revision>();
425
+ const revisions = updates.map((update) => {
426
+ const revision = mintRevision();
427
+
428
+ if (update.target === null) deleteRef.run(update.name);
429
+ else putRef.run(update.name, update.target, revision);
430
+ updateSessionIndex(database, update.name, update.target, revision);
431
+ const session = parseSessionRef(update.name);
432
+
433
+ if (session !== null) touched.set(session, revision);
434
+
435
+ return revision;
436
+ });
437
+
438
+ for (const [session, version] of touched)
439
+ database
440
+ .prepare("UPDATE session_index SET version = ? WHERE session = ?")
441
+ .run(version, session);
442
+
443
+ const at = Date.now();
444
+ const tails = new Map<SessionId, number>();
445
+
446
+ const appended = logged.map(({ session, event }) => {
447
+ const seq = (tails.get(session) ?? tail(session)) + 1;
448
+ tails.set(session, seq);
449
+ write(database, append, session, seq, at, JSON.stringify(event));
450
+
451
+ return asSeq(seq);
452
+ });
453
+
454
+ database.exec("COMMIT");
455
+ workers?.check();
456
+
457
+ return { kind: "set", logged: appended, at, revisions };
458
+ } catch (error) {
459
+ if (database.isTransaction) database.exec("ROLLBACK");
460
+
461
+ throw error;
462
+ } finally {
463
+ dev("sqlite", "transaction", {
464
+ connection: "writer",
465
+ lockWaitMs: acquired - waiting,
466
+ executionMs: performance.now() - acquired,
467
+ });
468
+ }
469
+ },
470
+ async log(session, after, limit) {
471
+ return (await read(readLog, session, after, limit)).map((row) =>
472
+ decodeEvent(parseNumber(row.seq), parseNumber(row.at), parseText(row.event) ?? ""),
473
+ );
474
+ },
475
+ tail: async (session) => tail(session),
476
+ },
477
+ checkpoints: {
478
+ async read(session) {
479
+ const [row] = await read(readCheckpoint, session);
480
+ const body = parseText(row?.body);
481
+
482
+ return row === undefined || body === null
483
+ ? null
484
+ : { seq: asSeq(parseNumber(row.seq)), body };
485
+ },
486
+ async write(session, checkpoint) {
487
+ write(database, writeCheckpoint, session, checkpoint.seq, checkpoint.body);
488
+ workers?.check();
489
+ },
490
+ async gap(session, after) {
491
+ const [row] = await read(readGap, session, after);
492
+
493
+ return { events: parseNumber(row?.events ?? 0), bytes: parseNumber(row?.bytes ?? 0) };
494
+ },
495
+ },
496
+ };
497
+ }
498
+
499
+ function updateSessionIndex(
500
+ database: DatabaseSync,
501
+ name: string,
502
+ target: ObjectId | null,
503
+ revision: Revision,
504
+ ): void {
505
+ if (sessionIndexRef(name) === null) return;
506
+ const body =
507
+ target === null
508
+ ? null
509
+ : parseText(database.prepare("SELECT body FROM objects WHERE id = ?").get(target)?.body);
510
+
511
+ for (const change of sessionIndexChanges(
512
+ name,
513
+ body === null ? null : decodeObject(body),
514
+ revision,
515
+ ))
516
+ database.prepare(change.text).run(...change.values);
517
+ }
518
+
519
+ function write(database: DatabaseSync, statement: StatementSync, ...params: SQLInputValue[]) {
520
+ const size = params.reduce<number>(
521
+ (total, value) =>
522
+ total +
523
+ (typeof value === "string" ? value.length : ArrayBuffer.isView(value) ? value.byteLength : 0),
524
+ 0,
525
+ );
526
+
527
+ return (size > 64 * 1024 ? database.prepare(statement.sourceSQL) : statement).run(...params);
528
+ }
@@ -0,0 +1,98 @@
1
+ import { resolve } from "node:path";
2
+ import process from "node:process";
3
+ import { DatabaseSync } from "node:sqlite";
4
+ import { SdkError } from "@nyte-ai/schema";
5
+ import { STORE_VERSION } from "@nyte-ai/core/store";
6
+ import { parseNumber, parseText } from "./store-rows.ts";
7
+
8
+ const CONTEND_MS = 1_000;
9
+
10
+ const SQLITE_BUSY = 5;
11
+
12
+ interface Claim {
13
+ readonly lock: DatabaseSync;
14
+ holders: number;
15
+ }
16
+
17
+ const claims = new Map<string, Claim>();
18
+
19
+ /** Another live process owns the store, so this one can't open it; the message names the owner. */
20
+ export class StoreOwned extends SdkError {
21
+ constructor(message: string) {
22
+ super("unavailable", message);
23
+ }
24
+ }
25
+
26
+ /**
27
+ * Makes this process the one owner of the file store at `path`, which alone opens,
28
+ * migrates and writes it, and returns what gives the claim up. The claim is a write lock
29
+ * this process holds on `<path>.owner`, a SQLite file whose one row names the owner's pid,
30
+ * program and store version; the operating system drops the lock when the process exits,
31
+ * so a crashed owner leaves no claim to clear. Claims of one path in this process share
32
+ * the lock until the last is given up. Throws `StoreOwned`, an `SdkError("unavailable")`
33
+ * naming the owner, when another process holds the claim.
34
+ */
35
+ export function claimStore(path: string): () => void {
36
+ const key = resolve(path);
37
+ const claim = claims.get(key) ?? { lock: lockStore(key), holders: 0 };
38
+ claims.set(key, claim);
39
+ claim.holders += 1;
40
+ let released = false;
41
+
42
+ return () => {
43
+ if (released) return;
44
+ released = true;
45
+ claim.holders -= 1;
46
+
47
+ if (claim.holders > 0) return;
48
+ claims.delete(key);
49
+ claim.lock.close();
50
+ };
51
+ }
52
+
53
+ function lockStore(path: string): DatabaseSync {
54
+ const lock = new DatabaseSync(`${path}.owner`, { timeout: CONTEND_MS });
55
+
56
+ try {
57
+ reserve(lock, path);
58
+ lock.exec(
59
+ "CREATE TABLE IF NOT EXISTS owner (id INTEGER PRIMARY KEY CHECK (id = 1), pid INTEGER NOT NULL, program TEXT NOT NULL, store INTEGER NOT NULL)",
60
+ );
61
+ lock
62
+ .prepare("INSERT OR REPLACE INTO owner (id, pid, program, store) VALUES (1, ?, ?, ?)")
63
+ .run(process.pid, process.execPath, STORE_VERSION);
64
+ lock.exec("COMMIT");
65
+ reserve(lock, path);
66
+ } catch (cause) {
67
+ lock.close();
68
+
69
+ throw cause;
70
+ }
71
+
72
+ return lock;
73
+ }
74
+
75
+ function reserve(lock: DatabaseSync, path: string): void {
76
+ try {
77
+ lock.exec("BEGIN IMMEDIATE");
78
+ } catch (cause) {
79
+ if (!(cause instanceof Error && "errcode" in cause && cause.errcode === SQLITE_BUSY))
80
+ throw cause;
81
+
82
+ throw new StoreOwned(
83
+ `${describeOwner(lock)} has ${path} open. Nyte opens a store in one process at a time; quit that one and try again.`,
84
+ );
85
+ }
86
+ }
87
+
88
+ function describeOwner(lock: DatabaseSync): string {
89
+ try {
90
+ const row = lock.prepare("SELECT pid, program, store FROM owner WHERE id = 1").get();
91
+
92
+ if (row === undefined) return "Another Nyte process";
93
+
94
+ return `Another Nyte process (pid ${String(parseNumber(row.pid))}, ${parseText(row.program)}, store version ${String(parseNumber(row.store))})`;
95
+ } catch {
96
+ return "Another Nyte process";
97
+ }
98
+ }