@graview/ship 0.1.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 (87) hide show
  1. package/LICENSE +96 -0
  2. package/README.md +95 -0
  3. package/dist/browser-adapter.d.ts +84 -0
  4. package/dist/browser-adapter.d.ts.map +1 -0
  5. package/dist/browser-adapter.js +111 -0
  6. package/dist/browser-adapter.js.map +1 -0
  7. package/dist/browser.d.ts +26 -0
  8. package/dist/browser.d.ts.map +1 -0
  9. package/dist/browser.js +16 -0
  10. package/dist/browser.js.map +1 -0
  11. package/dist/cli.d.ts +48 -0
  12. package/dist/cli.d.ts.map +1 -0
  13. package/dist/cli.js +170 -0
  14. package/dist/cli.js.map +1 -0
  15. package/dist/dev.d.ts +148 -0
  16. package/dist/dev.d.ts.map +1 -0
  17. package/dist/dev.js +286 -0
  18. package/dist/dev.js.map +1 -0
  19. package/dist/door.d.ts +22 -0
  20. package/dist/door.d.ts.map +1 -0
  21. package/dist/door.js +43 -0
  22. package/dist/door.js.map +1 -0
  23. package/dist/export.d.ts +25 -0
  24. package/dist/export.d.ts.map +1 -0
  25. package/dist/export.js +30 -0
  26. package/dist/export.js.map +1 -0
  27. package/dist/file-adapter.d.ts +28 -0
  28. package/dist/file-adapter.d.ts.map +1 -0
  29. package/dist/file-adapter.js +50 -0
  30. package/dist/file-adapter.js.map +1 -0
  31. package/dist/health.d.ts +19 -0
  32. package/dist/health.d.ts.map +1 -0
  33. package/dist/health.js +16 -0
  34. package/dist/health.js.map +1 -0
  35. package/dist/index.d.ts +29 -0
  36. package/dist/index.d.ts.map +1 -0
  37. package/dist/index.js +15 -0
  38. package/dist/index.js.map +1 -0
  39. package/dist/migrations.d.ts +31 -0
  40. package/dist/migrations.d.ts.map +1 -0
  41. package/dist/migrations.js +59 -0
  42. package/dist/migrations.js.map +1 -0
  43. package/dist/open-store.d.ts +57 -0
  44. package/dist/open-store.d.ts.map +1 -0
  45. package/dist/open-store.js +103 -0
  46. package/dist/open-store.js.map +1 -0
  47. package/dist/photos.d.ts +89 -0
  48. package/dist/photos.d.ts.map +1 -0
  49. package/dist/photos.js +102 -0
  50. package/dist/photos.js.map +1 -0
  51. package/dist/presence.d.ts +33 -0
  52. package/dist/presence.d.ts.map +1 -0
  53. package/dist/presence.js +99 -0
  54. package/dist/presence.js.map +1 -0
  55. package/dist/remote.d.ts +73 -0
  56. package/dist/remote.d.ts.map +1 -0
  57. package/dist/remote.js +271 -0
  58. package/dist/remote.js.map +1 -0
  59. package/dist/serve.d.ts +121 -0
  60. package/dist/serve.d.ts.map +1 -0
  61. package/dist/serve.js +249 -0
  62. package/dist/serve.js.map +1 -0
  63. package/dist/snapshot.d.ts +20 -0
  64. package/dist/snapshot.d.ts.map +1 -0
  65. package/dist/snapshot.js +41 -0
  66. package/dist/snapshot.js.map +1 -0
  67. package/dist/source-edit.d.ts +52 -0
  68. package/dist/source-edit.d.ts.map +1 -0
  69. package/dist/source-edit.js +455 -0
  70. package/dist/source-edit.js.map +1 -0
  71. package/dist/steps.d.ts +98 -0
  72. package/dist/steps.d.ts.map +1 -0
  73. package/dist/steps.js +189 -0
  74. package/dist/steps.js.map +1 -0
  75. package/dist/studio-door.d.ts +36 -0
  76. package/dist/studio-door.d.ts.map +1 -0
  77. package/dist/studio-door.js +102 -0
  78. package/dist/studio-door.js.map +1 -0
  79. package/dist/sync-seed.d.ts +61 -0
  80. package/dist/sync-seed.d.ts.map +1 -0
  81. package/dist/sync-seed.js +95 -0
  82. package/dist/sync-seed.js.map +1 -0
  83. package/dist/typecheck.d.ts +15 -0
  84. package/dist/typecheck.d.ts.map +1 -0
  85. package/dist/typecheck.js +59 -0
  86. package/dist/typecheck.js.map +1 -0
  87. package/package.json +75 -0
package/dist/remote.js ADDED
@@ -0,0 +1,271 @@
1
+ import { foldPresence, PRESENCE_TTL_MS, samePresence, Store, } from "@graview/core";
2
+ /**
3
+ * Opens a store from a server and keeps it in step with it.
4
+ *
5
+ * `seed` is deliberately absent: what is on the server is what there is. A
6
+ * client that seeded would be a client that could overwrite everybody's
7
+ * roster by being the first to load.
8
+ */
9
+ /** A counter nobody else is using: this client's own, provisional ids. */
10
+ function localIds() {
11
+ const who = Math.random().toString(36).slice(2, 8);
12
+ let at = 0;
13
+ return () => `local-${who}-${++at}`;
14
+ }
15
+ export async function openRemote(options) {
16
+ const call = options.fetch ?? fetch;
17
+ const headers = { "content-type": "application/json", ...(options.headers ?? {}) };
18
+ if (options.principal?.id)
19
+ headers["x-graview-seat"] = options.principal.id;
20
+ if (options.principal?.roles?.length)
21
+ headers["x-graview-roles"] = options.principal.roles.join(",");
22
+ const state = (await (await call(`${options.url}/graview/state`)).json());
23
+ const store = new Store({
24
+ schema: options.app.schema,
25
+ mutations: options.app.mutations ?? [],
26
+ invariants: options.app.invariants ?? [],
27
+ ...(options.app.policy ? { policy: options.app.policy } : {}),
28
+ ...(options.app.modules ? { modules: options.app.modules } : {}),
29
+ ...(options.app.intelligence ? { intelligence: options.app.intelligence } : {}),
30
+ /*
31
+ * The snapshot AND the log: the snapshot is the graph, the log is the
32
+ * history that led to it. Folding the log alone would lose whatever the
33
+ * server was seeded with, which was never an operation.
34
+ */
35
+ snapshot: state.snapshot,
36
+ log: state.log,
37
+ /*
38
+ * IDS THIS CLIENT CANNOT SHARE WITH ANYBODY.
39
+ *
40
+ * A store's default id is a monotonic counter, which is exactly right
41
+ * for a test and exactly wrong for a second writer: two stores both
42
+ * start at one, so this browser's first optimistic op and the server's
43
+ * first op are both `op1` — and `receive` then drops the server's as
44
+ * one it already had. The change silently never arrived, on whichever
45
+ * browser happened to have acted once.
46
+ *
47
+ * The ops this store mints are PROVISIONAL anyway: the server's own is
48
+ * what stays. Prefixing them says so, and makes the collision
49
+ * impossible rather than unlikely.
50
+ */
51
+ ids: localIds(),
52
+ ...(options.storeOptions ?? {}),
53
+ });
54
+ let seen = state.log.at(-1)?.seq ?? -1;
55
+ /*
56
+ * PRESENCE, beside the log and never in it. `mine` is the last word this
57
+ * browser said about itself; it goes out with every poll while it stands,
58
+ * and what comes back is folded into `known` for whoever is listening.
59
+ */
60
+ let mine = null;
61
+ let known = new Map();
62
+ const whoListeners = new Set();
63
+ const heard = (who) => {
64
+ const next = foldPresence(new Map(), who, Date.now(), PRESENCE_TTL_MS * 4, mine?.participant);
65
+ let changed = next.size !== known.size;
66
+ if (!changed)
67
+ for (const [participant, presence] of next)
68
+ if (!samePresence(presence, known.get(participant)))
69
+ changed = true;
70
+ known = next;
71
+ if (!changed)
72
+ return;
73
+ for (const listener of whoListeners)
74
+ listener([...known.values()]);
75
+ };
76
+ const land = (ops, applied = false) => {
77
+ if (ops.length === 0)
78
+ return [];
79
+ seen = Math.max(seen, ...ops.map((op) => op.seq));
80
+ return store.receive(ops, { applied });
81
+ };
82
+ const pull = async () => {
83
+ if (mine) {
84
+ const response = await call(`${options.url}/graview/here`, {
85
+ method: "POST",
86
+ headers,
87
+ body: JSON.stringify({ presence: mine, seq: seen }),
88
+ });
89
+ const { who, ops } = (await response.json());
90
+ heard(who ?? []);
91
+ return land(ops ?? []);
92
+ }
93
+ const response = await call(`${options.url}/graview/since?seq=${seen}`, { headers });
94
+ const { ops } = (await response.json());
95
+ return land(ops);
96
+ };
97
+ const presence = {
98
+ here(next) {
99
+ mine = next;
100
+ },
101
+ onWho(listener) {
102
+ whoListeners.add(listener);
103
+ return () => {
104
+ whoListeners.delete(listener);
105
+ };
106
+ },
107
+ leave() {
108
+ if (!mine)
109
+ return;
110
+ const body = JSON.stringify({ participant: mine.participant });
111
+ mine = null;
112
+ // A page on its way out gets one shot; a beacon is what survives it.
113
+ const beacon = globalThis.navigator?.sendBeacon;
114
+ if (beacon)
115
+ beacon.call(globalThis.navigator, `${options.url}/graview/leave`, body);
116
+ else
117
+ void call(`${options.url}/graview/leave`, { method: "POST", headers, body }).catch(() => { });
118
+ },
119
+ };
120
+ /** The server's batch for each provisional one this browser minted, so an undo names what the server has. */
121
+ const batches = new Map();
122
+ /** Every post not yet answered, so `settled` can wait for the verdicts. */
123
+ const inFlight = new Set();
124
+ const track = (promise) => {
125
+ inFlight.add(promise);
126
+ void promise.finally(() => inFlight.delete(promise)).catch(() => { });
127
+ return promise;
128
+ };
129
+ /*
130
+ * `applied` says the local store already holds this change — it was
131
+ * applied optimistically before the post — so the server's op is
132
+ * recorded rather than re-applied. `send` never applies first, so its
133
+ * answer lands in full.
134
+ */
135
+ const post = async (body, applied = false) => {
136
+ const response = await call(`${options.url}/graview/ops`, {
137
+ method: "POST",
138
+ headers,
139
+ body: JSON.stringify(body),
140
+ });
141
+ const answer = (await response.json());
142
+ if (!response.ok) {
143
+ // The policy's own sentence, carried across the wire unchanged: a
144
+ // refusal a person can read is the whole point of having one.
145
+ throw new Error(answer.error ?? `The server refused (${response.status})`);
146
+ }
147
+ const ops = answer.ops ?? [];
148
+ land(ops, applied);
149
+ return { ops, ...(answer.batch ? { batch: answer.batch } : {}) };
150
+ };
151
+ const send = async (calls, sending = {}) => (await post({ calls, ...sending })).ops;
152
+ /*
153
+ * APPLIED HERE FIRST, DECIDED THERE.
154
+ *
155
+ * `store.apply` is synchronous — the scene, the strip and the routed face
156
+ * all expect the graph to have moved by the time the press returns — and
157
+ * the network is not. So the local store applies optimistically and the
158
+ * server is told; what comes back is the SERVER's op, which is the one
159
+ * that stays, with the id, author and sequence everybody else will see.
160
+ *
161
+ * And a refusal takes the optimism back. The alternative — leaving the
162
+ * hopeful change on screen and logging the refusal to a console — is the
163
+ * one behaviour that would make this whole design a lie: the interface
164
+ * would be showing a graph the server does not have.
165
+ */
166
+ const appliedAll = store.applyAll.bind(store);
167
+ const undone = store.undo.bind(store);
168
+ const refusals = new Set();
169
+ /*
170
+ * A refusal takes the optimism back — undone by its own batch, which is
171
+ * the same mechanism a person's undo uses — AS THE SAME PERSON. What you
172
+ * may undo is what you may have done, and an undo judged as nobody is
173
+ * refused in an app with a policy: taking back your own optimism would
174
+ * have thrown a second, more confusing refusal on top of the first.
175
+ */
176
+ const takeBack = (batch, error) => {
177
+ const reason = error instanceof Error ? error.message : String(error);
178
+ try {
179
+ const asMe = options.principal ? { author: options.principal } : {};
180
+ if (store.canUndo(batch).ok)
181
+ undone(batch, asMe);
182
+ }
183
+ catch {
184
+ // A take-back that cannot run leaves the interface wrong, and
185
+ // saying so is still better than saying nothing.
186
+ }
187
+ for (const told of refusals)
188
+ told(reason);
189
+ };
190
+ /*
191
+ * EVERY WAY THE GRAPH CHANGES GOES DOWN THE WIRE. `apply` is one call;
192
+ * `applyAll` is a whole gesture — a seat's plan lands as one batch — and
193
+ * `undo` is a take-back. The first version patched `apply` alone, so a
194
+ * robot's plans and undos stayed in the browser that made them while its
195
+ * single presses travelled: two windows on one roster disagreed about
196
+ * exactly the changes an agent had made.
197
+ */
198
+ store.applyAll = ((calls, applyOptions) => {
199
+ /*
200
+ * As WHOEVER IS AT THIS KEYBOARD, by default.
201
+ *
202
+ * The local store carries the app's policy, so a call with no author is
203
+ * judged as an anonymous human — who, in an app with a policy, may do
204
+ * nothing. Every surface that threads a principal is unaffected; what
205
+ * this fixes is the one that does not, which would otherwise meet a
206
+ * refusal about a person who is not sitting there.
207
+ */
208
+ const result = appliedAll(calls, {
209
+ ...(options.principal ? { author: options.principal } : {}),
210
+ ...applyOptions,
211
+ });
212
+ track(post({ calls, ...(applyOptions?.intent ? { intent: applyOptions.intent } : {}) }, true)
213
+ .then((answer) => {
214
+ if (answer.batch)
215
+ batches.set(result.batch, answer.batch);
216
+ })
217
+ .catch((error) => takeBack(result.batch, error)));
218
+ return result;
219
+ });
220
+ // `apply` is `applyAll` of one — and it must go through the patched one.
221
+ store.apply = ((callMade, applyOptions) => store.applyAll([callMade], applyOptions));
222
+ store.undo = ((batchIds, undoOptions) => {
223
+ const ids = typeof batchIds === "string" ? [batchIds] : batchIds;
224
+ const result = undone(ids, {
225
+ ...(options.principal ? { author: options.principal } : {}),
226
+ ...undoOptions,
227
+ });
228
+ // Named as the server knows them: a provisional batch by the one it became.
229
+ const theirs = ids.map((id) => batches.get(id) ?? id);
230
+ track(post({ undo: theirs, ...(undoOptions?.intent ? { intent: undoOptions.intent } : {}) }, true)
231
+ .then((answer) => {
232
+ if (answer.batch)
233
+ batches.set(result.batch, answer.batch);
234
+ })
235
+ .catch((error) => takeBack(result.batch, error)));
236
+ return result;
237
+ });
238
+ const every = options.pollMs ?? 800;
239
+ const timer = every > 0 && typeof setInterval === "function"
240
+ ? setInterval(() => {
241
+ void pull().catch(() => {
242
+ // A poll that cannot reach the server is a poll that will try
243
+ // again; throwing here would take the interface down with the
244
+ // connection.
245
+ });
246
+ }, every)
247
+ : null;
248
+ return {
249
+ store,
250
+ version: state.version,
251
+ migrated: state.migrated ?? [],
252
+ send,
253
+ pull,
254
+ onRefusal(listener) {
255
+ refusals.add(listener);
256
+ return () => refusals.delete(listener);
257
+ },
258
+ async settled() {
259
+ // Whatever is in flight now — and whatever a settling handler put in flight after it.
260
+ while (inFlight.size > 0)
261
+ await Promise.allSettled([...inFlight]);
262
+ },
263
+ presence,
264
+ close() {
265
+ if (timer)
266
+ clearInterval(timer);
267
+ presence.leave();
268
+ },
269
+ };
270
+ }
271
+ //# sourceMappingURL=remote.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"remote.js","sourceRoot":"","sources":["../src/remote.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,121 @@
1
+ import { type IncomingMessage, type Server } from "node:http";
2
+ import { Store, type AnySchema, type GraviewApp, type Operation, type PersistenceAdapter, type Principal } from "@graview/core";
3
+ import { type OpenedStore } from "./open-store.js";
4
+ import type { GraphSnapshot } from "./snapshot.js";
5
+ /**
6
+ * THE STORE, BEHIND HTTP — and the op log is the wire.
7
+ *
8
+ * Two server-side adapters existed and nothing showed either, so the
9
+ * strongest thing this platform can say to somebody self-hosting — "where
10
+ * is my data" answered with a folder you can open — was a capability the
11
+ * launcher asserted rather than a capability anybody had seen.
12
+ *
13
+ * What is deliberately NOT here: a second way to change the graph. A client
14
+ * sends CALLS, not primitives; the server applies them through an ordinary
15
+ * `Store` under the principal the request carries, so the policy judges
16
+ * them exactly as it judges them in a browser, the invariants run, and the
17
+ * op that comes back is the same shape the browser would have made. A
18
+ * client that could post primitives would be a client that could write past
19
+ * every rule the app declares.
20
+ *
21
+ * The whole protocol is four routes:
22
+ *
23
+ * GET /graview/state the graph, the log and the stored version
24
+ * POST /graview/ops calls in, the ops they produced out — or `undo`, batches to take back
25
+ * GET /graview/since?seq=N the ops appended after N — everyone else's
26
+ * GET /graview/health ship's own report, plus where the data is
27
+ *
28
+ * And two for WHO IS HERE, which is not the op log and never touches it:
29
+ *
30
+ * POST /graview/here say where you are; answers with who else is, and the ops since `seq`
31
+ * GET /graview/who who is here right now
32
+ * POST /graview/leave say you have gone
33
+ *
34
+ * Presence lives in a map inside this closure with a time to live — not in
35
+ * the store, not in the adapter, not in the log. A tab that dies without a
36
+ * word is gone after three missed heartbeats.
37
+ *
38
+ * Polling rather than a socket, on purpose: a demo whose point is "your data
39
+ * is in this folder" should not also be a demonstration of connection
40
+ * lifecycles, and a poll is a thing a person can reproduce with `curl`.
41
+ */
42
+ /**
43
+ * THE WIRE, AS A CONTRACT. Every route `serveStore` answers, in one place a
44
+ * test pins and a README repeats: what a host in front of this — Graview
45
+ * Cloud, or anybody's — must keep answering for `openRemote`, `graview mcp
46
+ * --remote-url` and `graview apply --remote-url` to work unchanged. What a
47
+ * host adds (who a bearer token is, which tenant, how much) goes in
48
+ * `seatOf` and around these routes, never inside them.
49
+ */
50
+ export declare const WIRE: readonly [{
51
+ readonly method: "GET";
52
+ readonly path: "/graview/state";
53
+ readonly says: "the graph, the log and the stored version";
54
+ }, {
55
+ readonly method: "POST";
56
+ readonly path: "/graview/ops";
57
+ readonly says: "calls in, the ops they produced out — or `undo`, batches to take back; 409 with the policy's sentence when refused";
58
+ }, {
59
+ readonly method: "GET";
60
+ readonly path: "/graview/since";
61
+ readonly says: "the ops appended after ?seq=N — everyone else's";
62
+ }, {
63
+ readonly method: "GET";
64
+ readonly path: "/graview/health";
65
+ readonly says: "ship's own report, plus where the data is";
66
+ }, {
67
+ readonly method: "GET";
68
+ readonly path: "/graview/export";
69
+ readonly says: "the whole store as one bundle, the way out";
70
+ }, {
71
+ readonly method: "POST";
72
+ readonly path: "/graview/here";
73
+ readonly says: "say where you are; answers with who else is, and the ops since `seq`";
74
+ }, {
75
+ readonly method: "GET";
76
+ readonly path: "/graview/who";
77
+ readonly says: "who is here right now";
78
+ }, {
79
+ readonly method: "POST";
80
+ readonly path: "/graview/leave";
81
+ readonly says: "say you have gone";
82
+ }];
83
+ /** The headers a request carries its seat in. A host's `seatOf` may read others; these are what the framework's clients send. */
84
+ export declare const SEAT_HEADERS: {
85
+ readonly seat: "x-graview-seat";
86
+ readonly roles: "x-graview-roles";
87
+ };
88
+ export interface ServeOptions<S extends AnySchema> {
89
+ readonly app: GraviewApp<S>;
90
+ /** Where the graph lives. The file adapter by default — see `createFileAdapter`. */
91
+ readonly adapter: PersistenceAdapter<string>;
92
+ /** What to put in an empty store on first start. */
93
+ readonly seed?: GraphSnapshot;
94
+ readonly scope?: string;
95
+ readonly port?: number;
96
+ /**
97
+ * Who a request is, from its own headers.
98
+ *
99
+ * Not an authentication system — this is a demo server, and pretending
100
+ * otherwise would be worse than saying so. It reads `x-graview-seat` and
101
+ * `x-graview-roles` and hands the result to the store, which is enough to
102
+ * show the thing worth showing: the SAME policy refusing the same act on
103
+ * the server that refuses it in the browser.
104
+ */
105
+ readonly seatOf?: (request: IncomingMessage) => Principal;
106
+ /** Where the data is, for the health report to say out loud. */
107
+ readonly where?: string;
108
+ /** How long a presence stands after its last word. Three heartbeats by default. */
109
+ readonly presenceTtlMs?: number;
110
+ }
111
+ export interface ServedStore<S extends AnySchema> {
112
+ readonly store: Store<S>;
113
+ readonly opened: OpenedStore<S>;
114
+ readonly server: Server;
115
+ readonly port: number;
116
+ readonly url: string;
117
+ close(): Promise<void>;
118
+ }
119
+ export declare function serveStore<S extends AnySchema>(options: ServeOptions<S>): Promise<ServedStore<S>>;
120
+ export type { Operation };
121
+ //# sourceMappingURL=serve.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../src/serve.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,KAAK,eAAe,EAAE,KAAK,MAAM,EAAuB,MAAM,WAAW,CAAC;AACjG,OAAO,EAGL,KAAK,EACL,KAAK,SAAS,EACd,KAAK,UAAU,EAEf,KAAK,SAAS,EACd,KAAK,kBAAkB,EAEvB,KAAK,SAAS,EACf,MAAM,eAAe,CAAC;AAGvB,OAAO,EAAa,KAAK,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAC9D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAEnD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAEH;;;;;;;GAOG;AACH,eAAO,MAAM,IAAI;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EASP,CAAC;AAEX,iIAAiI;AACjI,eAAO,MAAM,YAAY;;;CAAgE,CAAC;AAE1F,MAAM,WAAW,YAAY,CAAC,CAAC,SAAS,SAAS;IAC/C,QAAQ,CAAC,GAAG,EAAE,UAAU,CAAC,CAAC,CAAC,CAAC;IAC5B,oFAAoF;IACpF,QAAQ,CAAC,OAAO,EAAE,kBAAkB,CAAC,MAAM,CAAC,CAAC;IAC7C,oDAAoD;IACpD,QAAQ,CAAC,IAAI,CAAC,EAAE,aAAa,CAAC;IAC9B,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB;;;;;;;;OAQG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,eAAe,KAAK,SAAS,CAAC;IAC1D,gEAAgE;IAChE,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,mFAAmF;IACnF,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;CACjC;AAED,MAAM,WAAW,WAAW,CAAC,CAAC,SAAS,SAAS;IAC9C,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC;IACzB,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC,CAAC,CAAC;IAChC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAiBD,wBAAsB,UAAU,CAAC,CAAC,SAAS,SAAS,EAAE,OAAO,EAAE,YAAY,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAyKvG;AAyBD,YAAY,EAAE,SAAS,EAAE,CAAC"}
package/dist/serve.js ADDED
@@ -0,0 +1,249 @@
1
+ import { createServer } from "node:http";
2
+ import { foldPresence, PRESENCE_TTL_MS, Store, } from "@graview/core";
3
+ import { exportBundle } from "./export.js";
4
+ import { health } from "./health.js";
5
+ import { openStore } from "./open-store.js";
6
+ /**
7
+ * THE STORE, BEHIND HTTP — and the op log is the wire.
8
+ *
9
+ * Two server-side adapters existed and nothing showed either, so the
10
+ * strongest thing this platform can say to somebody self-hosting — "where
11
+ * is my data" answered with a folder you can open — was a capability the
12
+ * launcher asserted rather than a capability anybody had seen.
13
+ *
14
+ * What is deliberately NOT here: a second way to change the graph. A client
15
+ * sends CALLS, not primitives; the server applies them through an ordinary
16
+ * `Store` under the principal the request carries, so the policy judges
17
+ * them exactly as it judges them in a browser, the invariants run, and the
18
+ * op that comes back is the same shape the browser would have made. A
19
+ * client that could post primitives would be a client that could write past
20
+ * every rule the app declares.
21
+ *
22
+ * The whole protocol is four routes:
23
+ *
24
+ * GET /graview/state the graph, the log and the stored version
25
+ * POST /graview/ops calls in, the ops they produced out — or `undo`, batches to take back
26
+ * GET /graview/since?seq=N the ops appended after N — everyone else's
27
+ * GET /graview/health ship's own report, plus where the data is
28
+ *
29
+ * And two for WHO IS HERE, which is not the op log and never touches it:
30
+ *
31
+ * POST /graview/here say where you are; answers with who else is, and the ops since `seq`
32
+ * GET /graview/who who is here right now
33
+ * POST /graview/leave say you have gone
34
+ *
35
+ * Presence lives in a map inside this closure with a time to live — not in
36
+ * the store, not in the adapter, not in the log. A tab that dies without a
37
+ * word is gone after three missed heartbeats.
38
+ *
39
+ * Polling rather than a socket, on purpose: a demo whose point is "your data
40
+ * is in this folder" should not also be a demonstration of connection
41
+ * lifecycles, and a poll is a thing a person can reproduce with `curl`.
42
+ */
43
+ /**
44
+ * THE WIRE, AS A CONTRACT. Every route `serveStore` answers, in one place a
45
+ * test pins and a README repeats: what a host in front of this — Graview
46
+ * Cloud, or anybody's — must keep answering for `openRemote`, `graview mcp
47
+ * --remote-url` and `graview apply --remote-url` to work unchanged. What a
48
+ * host adds (who a bearer token is, which tenant, how much) goes in
49
+ * `seatOf` and around these routes, never inside them.
50
+ */
51
+ export const WIRE = [
52
+ { method: "GET", path: "/graview/state", says: "the graph, the log and the stored version" },
53
+ { method: "POST", path: "/graview/ops", says: "calls in, the ops they produced out — or `undo`, batches to take back; 409 with the policy's sentence when refused" },
54
+ { method: "GET", path: "/graview/since", says: "the ops appended after ?seq=N — everyone else's" },
55
+ { method: "GET", path: "/graview/health", says: "ship's own report, plus where the data is" },
56
+ { method: "GET", path: "/graview/export", says: "the whole store as one bundle, the way out" },
57
+ { method: "POST", path: "/graview/here", says: "say where you are; answers with who else is, and the ops since `seq`" },
58
+ { method: "GET", path: "/graview/who", says: "who is here right now" },
59
+ { method: "POST", path: "/graview/leave", says: "say you have gone" },
60
+ ];
61
+ /** The headers a request carries its seat in. A host's `seatOf` may read others; these are what the framework's clients send. */
62
+ export const SEAT_HEADERS = { seat: "x-graview-seat", roles: "x-graview-roles" };
63
+ const SEAT = (request) => {
64
+ const id = header(request, "x-graview-seat");
65
+ const roles = header(request, "x-graview-roles");
66
+ return {
67
+ kind: "human",
68
+ ...(id ? { id } : {}),
69
+ ...(roles ? { roles: roles.split(",").filter(Boolean) } : {}),
70
+ };
71
+ };
72
+ function header(request, name) {
73
+ const value = request.headers[name];
74
+ return Array.isArray(value) ? value[0] : value;
75
+ }
76
+ export async function serveStore(options) {
77
+ const scope = options.scope ?? options.app.name;
78
+ const seatOf = options.seatOf ?? SEAT;
79
+ /*
80
+ * The migration runs HERE, once, against the stored graph — which is the
81
+ * whole reason a server is the honest place for it. A browser that
82
+ * migrates its own copy migrates it once per browser; a server that
83
+ * migrates the roster migrates the roster.
84
+ */
85
+ const opened = await openStore({
86
+ app: options.app,
87
+ adapter: options.adapter,
88
+ scope,
89
+ ...(options.seed ? { seed: options.seed } : {}),
90
+ });
91
+ const store = opened.store;
92
+ /*
93
+ * WHO IS HERE. Keyed by participant, named by the seat the request
94
+ * carries — a poster cannot claim to be somebody else's seat — and
95
+ * stamped with the server's clock, so expiry does not depend on two
96
+ * browsers agreeing what time it is.
97
+ */
98
+ const ttl = options.presenceTtlMs ?? PRESENCE_TTL_MS;
99
+ let here = new Map();
100
+ const alive = () => {
101
+ here = foldPresence(here, [], Date.now(), ttl);
102
+ return [...here.values()];
103
+ };
104
+ const arrive = (told, seat) => {
105
+ const session = told.participant.split(":").slice(2).join(":");
106
+ const participant = seat.id ? `${seat.kind}:${seat.id}:${session}` : told.participant;
107
+ const presence = { ...told, participant, at: new Date().toISOString() };
108
+ here = foldPresence(here, [presence], Date.now(), ttl);
109
+ return presence;
110
+ };
111
+ const since = (seq) => store.log.all().filter((op) => op.seq > seq);
112
+ const server = createServer((request, response) => {
113
+ void handle(request, response).catch((error) => {
114
+ send(response, 500, { error: error instanceof Error ? error.message : String(error) });
115
+ });
116
+ });
117
+ async function handle(request, response) {
118
+ // A browser on another origin is the ordinary case for an embed.
119
+ response.setHeader("access-control-allow-origin", "*");
120
+ response.setHeader("access-control-allow-headers", "content-type, authorization, x-graview-seat, x-graview-roles");
121
+ response.setHeader("access-control-allow-methods", "GET, POST, OPTIONS");
122
+ if (request.method === "OPTIONS") {
123
+ response.writeHead(204).end();
124
+ return;
125
+ }
126
+ const url = new URL(request.url ?? "/", "http://localhost");
127
+ if (url.pathname === "/graview/state") {
128
+ send(response, 200, {
129
+ version: options.app.version ?? 1,
130
+ snapshot: store.snapshot(),
131
+ log: store.log.all(),
132
+ migrated: opened.migrated.map((op) => op.intent),
133
+ });
134
+ return;
135
+ }
136
+ if (url.pathname === "/graview/since") {
137
+ const seq = Number(url.searchParams.get("seq") ?? "-1");
138
+ send(response, 200, { ops: since(seq) });
139
+ return;
140
+ }
141
+ if (url.pathname === "/graview/who") {
142
+ send(response, 200, { who: alive() });
143
+ return;
144
+ }
145
+ if (url.pathname === "/graview/here" && request.method === "POST") {
146
+ const body = (await read(request));
147
+ if (!body.presence || typeof body.presence.participant !== "string" || typeof body.presence.stop !== "string") {
148
+ send(response, 400, { error: "A presence is a participant and a stop." });
149
+ return;
150
+ }
151
+ const mine = arrive(body.presence, seatOf(request));
152
+ // Folded into the poll: the heartbeat carries back everybody else AND
153
+ // the ops since, so being here costs no round trip of its own.
154
+ send(response, 200, {
155
+ who: alive().filter((presence) => presence.participant !== mine.participant),
156
+ ...(typeof body.seq === "number" ? { ops: since(body.seq) } : {}),
157
+ });
158
+ return;
159
+ }
160
+ if (url.pathname === "/graview/leave" && request.method === "POST") {
161
+ const body = (await read(request));
162
+ if (typeof body.participant === "string") {
163
+ here = new Map(here);
164
+ here.delete(body.participant);
165
+ }
166
+ send(response, 200, { who: alive() });
167
+ return;
168
+ }
169
+ if (url.pathname === "/graview/health") {
170
+ send(response, 200, {
171
+ ...health(store, options.app),
172
+ // Said by the adapter where it knows, because it is the only thing
173
+ // that does; `where` is for a caller with a better name for it.
174
+ where: options.where ?? options.adapter.root ?? scope,
175
+ adapter: options.adapter.name,
176
+ });
177
+ return;
178
+ }
179
+ if (url.pathname === "/graview/export") {
180
+ send(response, 200, exportBundle(store, options.app));
181
+ return;
182
+ }
183
+ if (url.pathname === "/graview/ops" && request.method === "POST") {
184
+ const body = (await read(request));
185
+ const calls = body.calls ?? [];
186
+ const seat = seatOf(request);
187
+ try {
188
+ /*
189
+ * Through the STORE, under the requester's own seat. The policy
190
+ * refuses here exactly what it refuses in the browser — and the
191
+ * refusal comes back with the policy's own sentence rather than a
192
+ * bare 403, because that sentence is the product.
193
+ */
194
+ const result = body.undo
195
+ ? store.undo(body.undo, { author: seat, ...(body.intent ? { intent: body.intent } : {}) })
196
+ : store.applyAll(calls, {
197
+ author: seat,
198
+ ...(body.intent ? { intent: body.intent } : {}),
199
+ ...(body.batch ? { batch: body.batch } : {}),
200
+ });
201
+ await opened.flush();
202
+ send(response, 200, { ops: result.ops, batch: result.batch });
203
+ }
204
+ catch (error) {
205
+ send(response, 409, {
206
+ error: error instanceof Error ? error.message : String(error),
207
+ refused: true,
208
+ });
209
+ }
210
+ return;
211
+ }
212
+ send(response, 404, { error: `Nothing at ${url.pathname}` });
213
+ }
214
+ const port = await listen(server, options.port ?? 0);
215
+ return {
216
+ store,
217
+ opened,
218
+ server,
219
+ port,
220
+ url: `http://localhost:${port}`,
221
+ async close() {
222
+ await opened.flush();
223
+ opened.close();
224
+ await new Promise((done) => server.close(() => done()));
225
+ },
226
+ };
227
+ }
228
+ function send(response, status, body) {
229
+ const text = JSON.stringify(body);
230
+ response.writeHead(status, { "content-type": "application/json", "content-length": Buffer.byteLength(text) });
231
+ response.end(text);
232
+ }
233
+ async function read(request) {
234
+ const chunks = [];
235
+ for await (const chunk of request)
236
+ chunks.push(chunk);
237
+ const text = Buffer.concat(chunks).toString("utf8");
238
+ return text.length === 0 ? {} : JSON.parse(text);
239
+ }
240
+ function listen(server, port) {
241
+ return new Promise((ready, fail) => {
242
+ server.once("error", fail);
243
+ server.listen(port, () => {
244
+ const address = server.address();
245
+ ready(typeof address === "object" && address !== null ? address.port : port);
246
+ });
247
+ });
248
+ }
249
+ //# sourceMappingURL=serve.js.map