@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
@@ -0,0 +1,89 @@
1
+ import type { GraphReader } from "@graview/core";
2
+ /**
3
+ * PHOTOGRAPHS, AND THE QUOTA THEY WILL OTHERWISE EAT.
4
+ *
5
+ * A photograph off a modern phone is three to six megabytes. The browser
6
+ * adapter in this package keeps the whole graph — snapshot, log and meta —
7
+ * in `localStorage`, which is about five megabytes in total. So the failure
8
+ * mode of doing nothing is not "photographs are big"; it is that the
9
+ * twelfth photograph throws a `QuotaExceededError` somewhere inside a save
10
+ * and **the graph does not persist** — every node, every op, every hour
11
+ * anyone logged, gone on the next reload, because somebody photographed a
12
+ * gutter.
13
+ *
14
+ * That is a trap this package sets and therefore a trap this package should
15
+ * spring. Three defences, in order:
16
+ *
17
+ * 1. **Downscale before anything is stored** (`downscale`, in
18
+ * `@graview/primitives` where a canvas is at hand). Nothing a model is
19
+ * asked to look at needs 4032 pixels.
20
+ * 2. **A declared budget**, checked before the write, with the number and
21
+ * the reason both written down rather than discovered.
22
+ * 3. **A loud, catchable refusal** when the budget is gone — never a
23
+ * silently truncated graph. The person is told which photograph could
24
+ * not be kept and how much room is left.
25
+ *
26
+ * Everything here is pure, so it runs in the domain tier where the check
27
+ * belongs: before the act, not after the write.
28
+ *
29
+ * Found by a product that stores a photograph on a sighting. Its numbers
30
+ * are the defaults, and any of them can be said again.
31
+ */
32
+ /**
33
+ * What to spend on photographs, in bytes.
34
+ *
35
+ * `localStorage` is ~5 MB per origin and the adapter stores a snapshot AND
36
+ * an append-only log, so a graph's own text can easily be another megabyte
37
+ * after a few seasons. 2 MB for photographs leaves room for both and for
38
+ * the log to keep growing, which matters more: a lost photograph is an
39
+ * annoyance and a lost log is the whole record.
40
+ */
41
+ export declare const PHOTO_BUDGET_BYTES: number;
42
+ /** A single photograph over this is refused outright, before the budget. */
43
+ export declare const PHOTO_MAX_BYTES: number;
44
+ export declare class PhotoTooLarge extends Error {
45
+ readonly bytes: number;
46
+ readonly remaining: number;
47
+ constructor(message: string, bytes: number, remaining: number);
48
+ }
49
+ /**
50
+ * How many bytes a data URL actually costs in storage.
51
+ *
52
+ * Base64 is 4 characters per 3 bytes, and `localStorage` stores UTF-16, so
53
+ * a data URL costs roughly TWICE its character count in real terms on some
54
+ * engines. Counting characters and doubling is the pessimistic reading, and
55
+ * pessimistic is the right direction for a budget whose overrun destroys
56
+ * the graph.
57
+ */
58
+ export declare function storageBytes(dataUrl: string): number;
59
+ /** Where a graph keeps its photographs: a kind, and a field on it. */
60
+ export interface PhotoField {
61
+ readonly kind: string;
62
+ readonly field: string;
63
+ }
64
+ /** What every photograph in the graph is costing, together. */
65
+ export declare function photosUsed(graph: GraphReader<{
66
+ id: string;
67
+ kind: string;
68
+ } & Record<string, unknown>>, where: readonly PhotoField[]): number;
69
+ export interface PhotoBudget {
70
+ readonly total?: number;
71
+ readonly each?: number;
72
+ /** What the installation is called, for the refusal's own sentence. */
73
+ readonly of?: string;
74
+ /**
75
+ * What to do about it, in the product's own words — "Remove a photograph
76
+ * from an older sighting". A refusal that does not say what to do next is
77
+ * half a refusal, and only the product knows what the older thing is
78
+ * called.
79
+ */
80
+ readonly advice?: string;
81
+ }
82
+ /**
83
+ * Whether one more photograph fits, and a readable refusal if it does not.
84
+ *
85
+ * Called BEFORE the act that would store it. Throwing after the write is
86
+ * the bug this exists to prevent.
87
+ */
88
+ export declare function assertPhotoFits(dataUrl: string, alreadyUsed: number, budget?: PhotoBudget): void;
89
+ //# sourceMappingURL=photos.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"photos.d.ts","sourceRoot":"","sources":["../src/photos.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAEjD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAEH;;;;;;;;GAQG;AACH,eAAO,MAAM,kBAAkB,QAAkB,CAAC;AAElD,4EAA4E;AAC5E,eAAO,MAAM,eAAe,QAAa,CAAC;AAE1C,qBAAa,aAAc,SAAQ,KAAK;IAGpC,QAAQ,CAAC,KAAK,EAAE,MAAM;IACtB,QAAQ,CAAC,SAAS,EAAE,MAAM;gBAF1B,OAAO,EAAE,MAAM,EACN,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,MAAM;CAK7B;AAID;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAEpD;AAED,sEAAsE;AACtE,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAED,+DAA+D;AAC/D,wBAAgB,UAAU,CACxB,KAAK,EAAE,WAAW,CAAC;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,EAC1E,KAAK,EAAE,SAAS,UAAU,EAAE,GAC3B,MAAM,CAUR;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,uEAAuE;IACvE,QAAQ,CAAC,EAAE,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;OAKG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,GAAE,WAAgB,GAAG,IAAI,CAuBpG"}
package/dist/photos.js ADDED
@@ -0,0 +1,102 @@
1
+ /**
2
+ * PHOTOGRAPHS, AND THE QUOTA THEY WILL OTHERWISE EAT.
3
+ *
4
+ * A photograph off a modern phone is three to six megabytes. The browser
5
+ * adapter in this package keeps the whole graph — snapshot, log and meta —
6
+ * in `localStorage`, which is about five megabytes in total. So the failure
7
+ * mode of doing nothing is not "photographs are big"; it is that the
8
+ * twelfth photograph throws a `QuotaExceededError` somewhere inside a save
9
+ * and **the graph does not persist** — every node, every op, every hour
10
+ * anyone logged, gone on the next reload, because somebody photographed a
11
+ * gutter.
12
+ *
13
+ * That is a trap this package sets and therefore a trap this package should
14
+ * spring. Three defences, in order:
15
+ *
16
+ * 1. **Downscale before anything is stored** (`downscale`, in
17
+ * `@graview/primitives` where a canvas is at hand). Nothing a model is
18
+ * asked to look at needs 4032 pixels.
19
+ * 2. **A declared budget**, checked before the write, with the number and
20
+ * the reason both written down rather than discovered.
21
+ * 3. **A loud, catchable refusal** when the budget is gone — never a
22
+ * silently truncated graph. The person is told which photograph could
23
+ * not be kept and how much room is left.
24
+ *
25
+ * Everything here is pure, so it runs in the domain tier where the check
26
+ * belongs: before the act, not after the write.
27
+ *
28
+ * Found by a product that stores a photograph on a sighting. Its numbers
29
+ * are the defaults, and any of them can be said again.
30
+ */
31
+ /**
32
+ * What to spend on photographs, in bytes.
33
+ *
34
+ * `localStorage` is ~5 MB per origin and the adapter stores a snapshot AND
35
+ * an append-only log, so a graph's own text can easily be another megabyte
36
+ * after a few seasons. 2 MB for photographs leaves room for both and for
37
+ * the log to keep growing, which matters more: a lost photograph is an
38
+ * annoyance and a lost log is the whole record.
39
+ */
40
+ export const PHOTO_BUDGET_BYTES = 2 * 1024 * 1024;
41
+ /** A single photograph over this is refused outright, before the budget. */
42
+ export const PHOTO_MAX_BYTES = 400 * 1024;
43
+ export class PhotoTooLarge extends Error {
44
+ bytes;
45
+ remaining;
46
+ constructor(message, bytes, remaining) {
47
+ super(message);
48
+ this.bytes = bytes;
49
+ this.remaining = remaining;
50
+ this.name = "PhotoTooLarge";
51
+ }
52
+ }
53
+ const kb = (bytes) => `${Math.round(bytes / 1024)} kB`;
54
+ /**
55
+ * How many bytes a data URL actually costs in storage.
56
+ *
57
+ * Base64 is 4 characters per 3 bytes, and `localStorage` stores UTF-16, so
58
+ * a data URL costs roughly TWICE its character count in real terms on some
59
+ * engines. Counting characters and doubling is the pessimistic reading, and
60
+ * pessimistic is the right direction for a budget whose overrun destroys
61
+ * the graph.
62
+ */
63
+ export function storageBytes(dataUrl) {
64
+ return dataUrl.length * 2;
65
+ }
66
+ /** What every photograph in the graph is costing, together. */
67
+ export function photosUsed(graph, where) {
68
+ let total = 0;
69
+ for (const node of graph.allNodes()) {
70
+ for (const at of where) {
71
+ if (node.kind !== at.kind)
72
+ continue;
73
+ const photo = node[at.field];
74
+ if (typeof photo === "string")
75
+ total += storageBytes(photo);
76
+ }
77
+ }
78
+ return total;
79
+ }
80
+ /**
81
+ * Whether one more photograph fits, and a readable refusal if it does not.
82
+ *
83
+ * Called BEFORE the act that would store it. Throwing after the write is
84
+ * the bug this exists to prevent.
85
+ */
86
+ export function assertPhotoFits(dataUrl, alreadyUsed, budget = {}) {
87
+ const total = budget.total ?? PHOTO_BUDGET_BYTES;
88
+ const each = budget.each ?? PHOTO_MAX_BYTES;
89
+ const bytes = storageBytes(dataUrl);
90
+ if (bytes > each) {
91
+ throw new PhotoTooLarge(`That photograph is ${kb(bytes)} and the most one may take is ${kb(each)}. ` +
92
+ `It should have been made smaller before it got here — this is a bug rather than your fault.`, bytes, total - alreadyUsed);
93
+ }
94
+ const remaining = total - alreadyUsed;
95
+ if (bytes > remaining) {
96
+ throw new PhotoTooLarge(`There is no room for this photograph: it needs ${kb(bytes)} and ${kb(remaining)} is left of ` +
97
+ `the ${kb(total)} ${budget.of ?? "this installation"} keeps for photographs. ` +
98
+ `Everything else you have written down is safe. ` +
99
+ `${budget.advice ?? "Remove a photograph from an older one to make room."}`, bytes, remaining);
100
+ }
101
+ }
102
+ //# sourceMappingURL=photos.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"photos.js","sourceRoot":"","sources":["../src/photos.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAEH;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,GAAG,IAAI,GAAG,IAAI,CAAC;AAElD,4EAA4E;AAC5E,MAAM,CAAC,MAAM,eAAe,GAAG,GAAG,GAAG,IAAI,CAAC;AAE1C,MAAM,OAAO,aAAc,SAAQ,KAAK;IAG3B;IACA;IAHX,YACE,OAAe,EACN,KAAa,EACb,SAAiB;QAE1B,KAAK,CAAC,OAAO,CAAC,CAAC;QAHN,UAAK,GAAL,KAAK,CAAQ;QACb,cAAS,GAAT,SAAS,CAAQ;QAG1B,IAAI,CAAC,IAAI,GAAG,eAAe,CAAC;IAC9B,CAAC;CACF;AAED,MAAM,EAAE,GAAG,CAAC,KAAa,EAAE,EAAE,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC;AAE/D;;;;;;;;GAQG;AACH,MAAM,UAAU,YAAY,CAAC,OAAe;IAC1C,OAAO,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC;AAC5B,CAAC;AAQD,+DAA+D;AAC/D,MAAM,UAAU,UAAU,CACxB,KAA0E,EAC1E,KAA4B;IAE5B,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,QAAQ,EAAE,EAAE,CAAC;QACpC,KAAK,MAAM,EAAE,IAAI,KAAK,EAAE,CAAC;YACvB,IAAI,IAAI,CAAC,IAAI,KAAK,EAAE,CAAC,IAAI;gBAAE,SAAS;YACpC,MAAM,KAAK,GAAG,IAAI,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC;YAC7B,IAAI,OAAO,KAAK,KAAK,QAAQ;gBAAE,KAAK,IAAI,YAAY,CAAC,KAAK,CAAC,CAAC;QAC9D,CAAC;IACH,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAgBD;;;;;GAKG;AACH,MAAM,UAAU,eAAe,CAAC,OAAe,EAAE,WAAmB,EAAE,SAAsB,EAAE;IAC5F,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,IAAI,kBAAkB,CAAC;IACjD,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,IAAI,eAAe,CAAC;IAC5C,MAAM,KAAK,GAAG,YAAY,CAAC,OAAO,CAAC,CAAC;IACpC,IAAI,KAAK,GAAG,IAAI,EAAE,CAAC;QACjB,MAAM,IAAI,aAAa,CACrB,sBAAsB,EAAE,CAAC,KAAK,CAAC,iCAAiC,EAAE,CAAC,IAAI,CAAC,IAAI;YAC1E,6FAA6F,EAC/F,KAAK,EACL,KAAK,GAAG,WAAW,CACpB,CAAC;IACJ,CAAC;IACD,MAAM,SAAS,GAAG,KAAK,GAAG,WAAW,CAAC;IACtC,IAAI,KAAK,GAAG,SAAS,EAAE,CAAC;QACtB,MAAM,IAAI,aAAa,CACrB,kDAAkD,EAAE,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC,SAAS,CAAC,cAAc;YAC5F,OAAO,EAAE,CAAC,KAAK,CAAC,IAAI,MAAM,CAAC,EAAE,IAAI,mBAAmB,0BAA0B;YAC9E,iDAAiD;YACjD,GAAG,MAAM,CAAC,MAAM,IAAI,qDAAqD,EAAE,EAC7E,KAAK,EACL,SAAS,CACV,CAAC;IACJ,CAAC;AACH,CAAC"}
@@ -0,0 +1,33 @@
1
+ import { type PresenceChannel } from "@graview/core";
2
+ /**
3
+ * WHO IS HERE, BETWEEN THE TABS OF ONE ORIGIN.
4
+ *
5
+ * A BroadcastChannel is multiplayer for free: two tabs on the same app see
6
+ * each other with no server, and a driven browser's pages share one when
7
+ * they share a context, which is what makes the harness deterministic. It
8
+ * sits beside `createBrowserAdapter` and, like it, never touches the store,
9
+ * the adapter or the log — presence is a fact about a person at a screen,
10
+ * and the graph is not where those go.
11
+ *
12
+ * Scoped by the store's scope, because a channel is per origin: a launcher
13
+ * with three apps mounted would otherwise put Things' keeper in the Colts'
14
+ * dressing room.
15
+ */
16
+ /** What this needs of a channel — the browser's own, or Node's, or a fake. */
17
+ export interface ChannelLike {
18
+ postMessage(message: unknown): void;
19
+ onmessage: ((event: {
20
+ readonly data: unknown;
21
+ }) => void) | null;
22
+ close(): void;
23
+ }
24
+ export interface BroadcastPresenceOptions {
25
+ /** How long a presence stands after its last word. Three heartbeats by default. */
26
+ readonly ttlMs?: number;
27
+ /** The channel itself, for a test or a host with its own. Defaults to `new BroadcastChannel(name)`. */
28
+ readonly channel?: ChannelLike;
29
+ readonly now?: () => number;
30
+ }
31
+ export declare const presenceChannelName: (scope: string) => string;
32
+ export declare function createBroadcastPresence(scope: string, options?: BroadcastPresenceOptions): PresenceChannel;
33
+ //# sourceMappingURL=presence.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"presence.d.ts","sourceRoot":"","sources":["../src/presence.ts"],"names":[],"mappings":"AAAA,OAAO,EAA8D,KAAK,eAAe,EAAE,MAAM,eAAe,CAAC;AAEjH;;;;;;;;;;;;;GAaG;AAEH,8EAA8E;AAC9E,MAAM,WAAW,WAAW;IAC1B,WAAW,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;IACpC,SAAS,EAAE,CAAC,CAAC,KAAK,EAAE;QAAE,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAA;KAAE,KAAK,IAAI,CAAC,GAAG,IAAI,CAAC;IAChE,KAAK,IAAI,IAAI,CAAC;CACf;AAED,MAAM,WAAW,wBAAwB;IACvC,mFAAmF;IACnF,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,uGAAuG;IACvG,QAAQ,CAAC,OAAO,CAAC,EAAE,WAAW,CAAC;IAC/B,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CAC7B;AAID,eAAO,MAAM,mBAAmB,GAAI,OAAO,MAAM,KAAG,MAAgC,CAAC;AAWrF,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,GAAE,wBAA6B,GAAG,eAAe,CA8E9G"}
@@ -0,0 +1,99 @@
1
+ import { foldPresence, PRESENCE_TTL_MS, samePresence } from "@graview/core";
2
+ export const presenceChannelName = (scope) => `graview:who:${scope}`;
3
+ function openChannel(name) {
4
+ const Channel = globalThis.BroadcastChannel;
5
+ if (!Channel)
6
+ return null;
7
+ const channel = new Channel(name);
8
+ // Node's channel keeps the process alive; a presence channel must not.
9
+ channel.unref?.();
10
+ return channel;
11
+ }
12
+ export function createBroadcastPresence(scope, options = {}) {
13
+ const ttl = options.ttlMs ?? PRESENCE_TTL_MS;
14
+ const now = options.now ?? (() => Date.now());
15
+ const channel = options.channel ?? openChannel(presenceChannelName(scope));
16
+ const listeners = new Set();
17
+ let known = new Map();
18
+ let self;
19
+ let sweep = null;
20
+ const tell = () => {
21
+ const who = [...known.values()];
22
+ for (const listener of listeners)
23
+ listener(who);
24
+ };
25
+ const changed = (next) => {
26
+ if (next.size !== known.size)
27
+ return true;
28
+ for (const [participant, presence] of next)
29
+ if (!samePresence(presence, known.get(participant)))
30
+ return true;
31
+ return false;
32
+ };
33
+ const fold = (arrived) => {
34
+ const next = foldPresence(known, arrived, now(), ttl, self);
35
+ // The fresh timestamps are kept EVEN WHEN nothing else moved: a heartbeat
36
+ // that says nothing new is exactly what keeps somebody here past the TTL.
37
+ const moved = changed(next);
38
+ known = next;
39
+ if (moved)
40
+ tell();
41
+ };
42
+ /* The sweep only runs while somebody is listening and somebody is here: a quiet page holds no timer. */
43
+ const keep = () => {
44
+ if (sweep || listeners.size === 0)
45
+ return;
46
+ sweep = setInterval(() => {
47
+ if (known.size === 0 && sweep) {
48
+ clearInterval(sweep);
49
+ sweep = null;
50
+ return;
51
+ }
52
+ fold([]);
53
+ }, Math.max(100, Math.floor(ttl / 2)));
54
+ sweep.unref?.();
55
+ };
56
+ if (channel) {
57
+ channel.onmessage = (event) => {
58
+ const word = event.data;
59
+ if (word?.type === "here") {
60
+ fold([word.presence]);
61
+ keep();
62
+ }
63
+ else if (word?.type === "leave") {
64
+ if (!known.has(word.participant))
65
+ return;
66
+ known = new Map(known);
67
+ known.delete(word.participant);
68
+ tell();
69
+ }
70
+ };
71
+ }
72
+ return {
73
+ here(presence) {
74
+ self = presence.participant;
75
+ // Never a word about yourself in your own map.
76
+ if (known.has(presence.participant))
77
+ fold([]);
78
+ channel?.postMessage({ type: "here", presence });
79
+ },
80
+ onWho(listener) {
81
+ listeners.add(listener);
82
+ keep();
83
+ return () => {
84
+ listeners.delete(listener);
85
+ };
86
+ },
87
+ leave() {
88
+ if (self !== undefined)
89
+ channel?.postMessage({ type: "leave", participant: self });
90
+ if (sweep)
91
+ clearInterval(sweep);
92
+ sweep = null;
93
+ listeners.clear();
94
+ known = new Map();
95
+ channel?.close();
96
+ },
97
+ };
98
+ }
99
+ //# sourceMappingURL=presence.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"presence.js","sourceRoot":"","sources":["../src/presence.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,73 @@
1
+ import { Store, type AnySchema, type GraviewApp, type MutationCall, type Operation, type PresenceChannel, type Principal } from "@graview/core";
2
+ /**
3
+ * THE OTHER END OF THE WIRE: a store whose graph lives on a server.
4
+ *
5
+ * The browser still holds a real `Store` — the scene, the strip, the rules
6
+ * and the routed face all read it the way they always have — but it does not
7
+ * own the truth. A call goes to the server, which judges it under this
8
+ * seat's principal and answers with the op it produced; the op is `receive`d
9
+ * here, so it lands with its own id, author and sequence and the interface
10
+ * updates exactly as it does for a local change.
11
+ *
12
+ * Everyone else's ops arrive the same way, on a poll. Two browsers open on
13
+ * the same roster see each other within a second, and neither of them has a
14
+ * second way to write the graph.
15
+ *
16
+ * Undo is the interesting case and it needed no special handling: `undo`
17
+ * produces ordinary calls, and they go down the same wire and are judged by
18
+ * the same policy — what you may undo is what you may have done, decided on
19
+ * the server rather than trusted from the client.
20
+ */
21
+ export interface RemoteOptions<S extends AnySchema> {
22
+ readonly app: GraviewApp<S>;
23
+ /** Where the server is: `http://localhost:5196`, say. */
24
+ readonly url: string;
25
+ /** Who is at this keyboard. Sent with every call; the server judges by it. */
26
+ readonly principal?: Principal;
27
+ /** How often to ask for everybody else's ops, in milliseconds. 0 never asks. */
28
+ readonly pollMs?: number;
29
+ readonly fetch?: typeof fetch;
30
+ readonly storeOptions?: Record<string, unknown>;
31
+ /**
32
+ * Headers sent with every request — a gateway's `authorization`, a
33
+ * tenant, whatever the host in front of `graview serve` asks for. The
34
+ * framework never reads them; the host's `seatOf` does. This is the seam
35
+ * where a hosted store's own auth goes without the framework knowing it.
36
+ */
37
+ readonly headers?: Readonly<Record<string, string>>;
38
+ }
39
+ export interface RemoteStore<S extends AnySchema> {
40
+ readonly store: Store<S>;
41
+ /** The server's version, and what opening it had to migrate. */
42
+ readonly version: number;
43
+ readonly migrated: readonly string[];
44
+ /** Send calls to the server and take back the ops it made. Throws the policy's own sentence. */
45
+ send(calls: readonly MutationCall[], options?: {
46
+ intent?: string;
47
+ batch?: string;
48
+ }): Promise<readonly Operation[]>;
49
+ /** Ask for everything appended since this browser last looked. */
50
+ pull(): Promise<readonly Operation[]>;
51
+ /**
52
+ * Told when the server refused something this browser had already shown.
53
+ * The change is taken back before the listener runs; what is left is
54
+ * saying so, in the policy's own words.
55
+ */
56
+ onRefusal(listener: (reason: string) => void): () => void;
57
+ /**
58
+ * Resolves once every call sent so far has been answered — accepted and
59
+ * landed, or refused and taken back. A browser never waits for this; a
60
+ * host that must report the server's verdict before it exits (an MCP
61
+ * seat, `graview apply`) does.
62
+ */
63
+ settled(): Promise<void>;
64
+ /**
65
+ * WHO IS HERE, over the same poll. Saying where you are rides on the next
66
+ * heartbeat and the answer carries everybody else — no round trip of its
67
+ * own, nothing written to the store, the adapter or the log.
68
+ */
69
+ readonly presence: PresenceChannel;
70
+ close(): void;
71
+ }
72
+ export declare function openRemote<S extends AnySchema>(options: RemoteOptions<S>): Promise<RemoteStore<S>>;
73
+ //# sourceMappingURL=remote.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"remote.d.ts","sourceRoot":"","sources":["../src/remote.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,EACL,KAAK,SAAS,EACd,KAAK,UAAU,EACf,KAAK,YAAY,EACjB,KAAK,SAAS,EAEd,KAAK,eAAe,EACpB,KAAK,SAAS,EACf,MAAM,eAAe,CAAC;AAGvB;;;;;;;;;;;;;;;;;;GAkBG;AAEH,MAAM,WAAW,aAAa,CAAC,CAAC,SAAS,SAAS;IAChD,QAAQ,CAAC,GAAG,EAAE,UAAU,CAAC,CAAC,CAAC,CAAC;IAC5B,yDAAyD;IACzD,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,8EAA8E;IAC9E,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,CAAC;IAC/B,gFAAgF;IAChF,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;IAC9B,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChD;;;;;OAKG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;CACrD;AAED,MAAM,WAAW,WAAW,CAAC,CAAC,SAAS,SAAS;IAC9C,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC;IACzB,gEAAgE;IAChE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC,gGAAgG;IAChG,IAAI,CAAC,KAAK,EAAE,SAAS,YAAY,EAAE,EAAE,OAAO,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,SAAS,SAAS,EAAE,CAAC,CAAC;IACnH,kEAAkE;IAClE,IAAI,IAAI,OAAO,CAAC,SAAS,SAAS,EAAE,CAAC,CAAC;IACtC;;;;OAIG;IACH,SAAS,CAAC,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;IAC1D;;;;;OAKG;IACH,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACzB;;;;OAIG;IACH,QAAQ,CAAC,QAAQ,EAAE,eAAe,CAAC;IACnC,KAAK,IAAI,IAAI,CAAC;CACf;AAgBD,wBAAsB,UAAU,CAAC,CAAC,SAAS,SAAS,EAAE,OAAO,EAAE,aAAa,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CA0QxG"}