@brass-build/client 0.2.0 → 0.4.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 (168) hide show
  1. package/AGENTS.md +26 -5
  2. package/CHANGELOG.md +61 -2
  3. package/README.md +247 -87
  4. package/dist/acting-organization.d.ts +131 -0
  5. package/dist/acting-organization.d.ts.map +1 -0
  6. package/dist/acting-organization.js +278 -0
  7. package/dist/acting-organization.js.map +1 -0
  8. package/dist/api-headers.js +7 -7
  9. package/dist/api-headers.js.map +1 -1
  10. package/dist/call-service.d.ts +74 -0
  11. package/dist/call-service.d.ts.map +1 -0
  12. package/dist/call-service.js +253 -0
  13. package/dist/call-service.js.map +1 -0
  14. package/dist/client.d.ts +29 -0
  15. package/dist/client.d.ts.map +1 -1
  16. package/dist/client.js +583 -12
  17. package/dist/client.js.map +1 -1
  18. package/dist/document-checkpoints.d.ts +1 -0
  19. package/dist/document-checkpoints.d.ts.map +1 -1
  20. package/dist/document-checkpoints.js +27 -0
  21. package/dist/document-checkpoints.js.map +1 -1
  22. package/dist/document-contents.d.ts +15 -0
  23. package/dist/document-contents.d.ts.map +1 -0
  24. package/dist/document-contents.js +88 -0
  25. package/dist/document-contents.js.map +1 -0
  26. package/dist/document-signals.d.ts +31 -1
  27. package/dist/document-signals.d.ts.map +1 -1
  28. package/dist/document-signals.js +59 -0
  29. package/dist/document-signals.js.map +1 -1
  30. package/dist/document-streams.d.ts +4 -2
  31. package/dist/document-streams.d.ts.map +1 -1
  32. package/dist/document-streams.js +6 -0
  33. package/dist/document-streams.js.map +1 -1
  34. package/dist/document-thumbnail-capture.d.ts +20 -0
  35. package/dist/document-thumbnail-capture.d.ts.map +1 -1
  36. package/dist/document-thumbnail-capture.js +247 -9
  37. package/dist/document-thumbnail-capture.js.map +1 -1
  38. package/dist/documents-namespace.d.ts.map +1 -1
  39. package/dist/documents-namespace.js +3 -0
  40. package/dist/documents-namespace.js.map +1 -1
  41. package/dist/documents.d.ts +51 -3
  42. package/dist/documents.d.ts.map +1 -1
  43. package/dist/documents.js +415 -36
  44. package/dist/documents.js.map +1 -1
  45. package/dist/export-guest.d.ts.map +1 -1
  46. package/dist/export-guest.js +5 -0
  47. package/dist/export-guest.js.map +1 -1
  48. package/dist/handle-types.d.ts +23 -0
  49. package/dist/handle-types.d.ts.map +1 -1
  50. package/dist/handles.d.ts +1 -1
  51. package/dist/handles.d.ts.map +1 -1
  52. package/dist/handles.js +3 -3
  53. package/dist/handles.js.map +1 -1
  54. package/dist/hosted-ui.d.ts +10 -0
  55. package/dist/hosted-ui.d.ts.map +1 -1
  56. package/dist/hosted-ui.js +115 -30
  57. package/dist/hosted-ui.js.map +1 -1
  58. package/dist/importer.d.ts.map +1 -1
  59. package/dist/importer.js +28 -66
  60. package/dist/importer.js.map +1 -1
  61. package/dist/index.d.ts +8 -3
  62. package/dist/index.d.ts.map +1 -1
  63. package/dist/index.js +25 -1
  64. package/dist/index.js.map +1 -1
  65. package/dist/internal.d.ts +2 -0
  66. package/dist/internal.d.ts.map +1 -1
  67. package/dist/internal.js +2 -0
  68. package/dist/internal.js.map +1 -1
  69. package/dist/mutate-batching.js +2 -2
  70. package/dist/mutate-batching.js.map +1 -1
  71. package/dist/open-document.d.ts +16 -1
  72. package/dist/open-document.d.ts.map +1 -1
  73. package/dist/open-document.js +123 -35
  74. package/dist/open-document.js.map +1 -1
  75. package/dist/presence.d.ts +10 -0
  76. package/dist/presence.d.ts.map +1 -1
  77. package/dist/presence.js.map +1 -1
  78. package/dist/record-paging.d.ts +13 -14
  79. package/dist/record-paging.d.ts.map +1 -1
  80. package/dist/record-paging.js +13 -14
  81. package/dist/record-paging.js.map +1 -1
  82. package/dist/serializer.d.ts +3 -3
  83. package/dist/session-toolbar.d.ts +9 -0
  84. package/dist/session-toolbar.d.ts.map +1 -1
  85. package/dist/session-toolbar.js +191 -21
  86. package/dist/session-toolbar.js.map +1 -1
  87. package/dist/tail-ops.js +2 -2
  88. package/dist/tail-ops.js.map +1 -1
  89. package/dist/types.d.ts +0 -42
  90. package/dist/types.d.ts.map +1 -1
  91. package/dist/undo.d.ts +1 -1
  92. package/dist/undo.d.ts.map +1 -1
  93. package/dist/undo.js +1 -1
  94. package/dist/undo.js.map +1 -1
  95. package/dist/version-bar.d.ts.map +1 -1
  96. package/dist/version-bar.js +75 -4
  97. package/dist/version-bar.js.map +1 -1
  98. package/dist/version.d.ts +2 -2
  99. package/dist/version.js +1 -1
  100. package/dist/wasm-ready-gate.d.ts +13 -0
  101. package/dist/wasm-ready-gate.d.ts.map +1 -0
  102. package/dist/wasm-ready-gate.js +28 -0
  103. package/dist/wasm-ready-gate.js.map +1 -0
  104. package/dist/wasm-runtime.browser.d.ts +11 -0
  105. package/dist/wasm-runtime.browser.d.ts.map +1 -0
  106. package/dist/wasm-runtime.browser.js +44 -0
  107. package/dist/wasm-runtime.browser.js.map +1 -0
  108. package/dist/wasm-runtime.d.ts +6 -0
  109. package/dist/wasm-runtime.d.ts.map +1 -0
  110. package/dist/wasm-runtime.js +20 -0
  111. package/dist/wasm-runtime.js.map +1 -0
  112. package/dist/when-saved.d.ts +2 -0
  113. package/dist/when-saved.d.ts.map +1 -0
  114. package/dist/when-saved.js +70 -0
  115. package/dist/when-saved.js.map +1 -0
  116. package/dist/ws-client.d.ts +4 -0
  117. package/dist/ws-client.d.ts.map +1 -1
  118. package/dist/ws-client.js +70 -1
  119. package/dist/ws-client.js.map +1 -1
  120. package/dist/ws-frame-limit.d.ts.map +1 -1
  121. package/dist/ws-frame-limit.js +9 -10
  122. package/dist/ws-frame-limit.js.map +1 -1
  123. package/docs/app-manifest.md +335 -5
  124. package/docs/custom-chrome.md +7 -2
  125. package/docs/presence.md +12 -0
  126. package/docs/services.md +265 -0
  127. package/docs/text-fields.md +191 -0
  128. package/package.json +4 -5
  129. package/src/acting-organization.ts +396 -0
  130. package/src/api-headers.ts +7 -7
  131. package/src/call-service.ts +354 -0
  132. package/src/client.ts +655 -15
  133. package/src/document-checkpoints.ts +30 -0
  134. package/src/document-contents.ts +104 -0
  135. package/src/document-signals.ts +113 -1
  136. package/src/document-streams.ts +15 -1
  137. package/src/document-thumbnail-capture.ts +253 -9
  138. package/src/documents-namespace.ts +20 -10
  139. package/src/documents.ts +460 -33
  140. package/src/export-guest.ts +5 -0
  141. package/src/handle-types.ts +35 -0
  142. package/src/handles.ts +7 -6
  143. package/src/hosted-ui.ts +155 -29
  144. package/src/importer.ts +28 -62
  145. package/src/index.ts +43 -4
  146. package/src/internal.ts +10 -0
  147. package/src/mutate-batching.ts +2 -2
  148. package/src/open-document.ts +179 -39
  149. package/src/presence.ts +6 -0
  150. package/src/record-paging.ts +13 -14
  151. package/src/serializer.ts +3 -3
  152. package/src/session-toolbar.ts +219 -22
  153. package/src/tail-ops.ts +2 -2
  154. package/src/types.ts +6 -4
  155. package/src/undo.ts +1 -1
  156. package/src/version-bar.ts +77 -7
  157. package/src/version.ts +1 -1
  158. package/src/wasm-ready-gate.ts +30 -0
  159. package/src/wasm-runtime.browser.ts +48 -0
  160. package/src/wasm-runtime.ts +21 -0
  161. package/src/when-saved.ts +66 -0
  162. package/src/ws-client.ts +70 -1
  163. package/src/ws-frame-limit.ts +9 -10
  164. package/dist/vite.d.ts +0 -31
  165. package/dist/vite.d.ts.map +0 -1
  166. package/dist/vite.js +0 -161
  167. package/dist/vite.js.map +0 -1
  168. package/src/vite.ts +0 -214
@@ -0,0 +1,48 @@
1
+ // The browser half of the WASM runtime seam: the ONE module in this package
2
+ // that names a WASM-shipping dependency, and the only place a new one is
3
+ // added. Everything else imports `./wasm-runtime.js`, whose node half this
4
+ // one replaces in a browser build, so no other file in the SDK (and no line of
5
+ // an app's own config) names a WASM dep or its loader.
6
+ //
7
+ // Each entry is loaded through its package's WEB build, which readies its
8
+ // module asynchronously behind an explicit init and locates its bytes with
9
+ // `new URL(..., import.meta.url)`. That combination is what makes the whole
10
+ // package build with no bundler plugins: every major bundler rewrites that URL
11
+ // into an emitted asset on its own, and nothing here emits a top-level await,
12
+ // so no build target has to accept one.
13
+ //
14
+ // The alternative each package offers by default is worse in a way nothing
15
+ // reports. A default browser entry fetches its module with a SYNCHRONOUS
16
+ // XMLHttpRequest while it evaluates, so an app freezes its main thread for the
17
+ // whole download before it paints, whether or not anyone opens a document, and
18
+ // the freeze grows with the reader's latency rather than with the app. That is
19
+ // what a browser build resolves to unless something selects otherwise, it
20
+ // builds clean, and the only tell is the chunk count.
21
+
22
+ // The init comes from the deeper path because the package's web entry
23
+ // re-exports its module with `export *`, which does not carry a default, so
24
+ // the entry's types do not declare the init its JavaScript exports. The two
25
+ // specifiers resolve to the same module instance (the entry imports this
26
+ // file), so readying through one readies the classes taken from the other.
27
+ import initEngine from 'loro-crdt/web/loro_wasm.js';
28
+
29
+ import { createReadyGate } from './wasm-ready-gate.js';
30
+
31
+ export * from 'loro-crdt/web';
32
+
33
+ // Every WASM module this package needs ready before it runs, as the init each
34
+ // one exports. Add an entry rather than replacing the list: a consumer with
35
+ // two installed during a migration window needs both readied, and the gate
36
+ // below is what every caller already awaits, so a new entry costs no new call
37
+ // anywhere.
38
+ const INITS: readonly (() => Promise<unknown>)[] = [initEngine];
39
+
40
+ /**
41
+ * Resolve once every WASM module this package needs is ready to run.
42
+ *
43
+ * `Brass.init()` starts it and every path that constructs a document awaits
44
+ * it, so an app that follows the documented boot never calls it. It is
45
+ * idempotent and concurrent-safe for the paths that construct documents
46
+ * without one (a test, an in-repo tool).
47
+ */
48
+ export const ensureWasmReady: () => Promise<void> = createReadyGate(INITS);
@@ -0,0 +1,21 @@
1
+ // The node half of the WASM runtime seam. Node resolves each WASM-shipping
2
+ // dependency to its own node build, which readies its module as it is
3
+ // imported, so there is nothing to await and `ensureWasmReady` is the same
4
+ // call answering immediately.
5
+ //
6
+ // This is the half every importer names, so a build that applies no
7
+ // substitution at all (tsc, a node-targeting esbuild, the test runner) is
8
+ // already correct, and the browser half's `import.meta.url` asset resolution
9
+ // never reaches a bundler targeting node. `package.json`'s `browser` map
10
+ // swaps in the browser half for a bundler building this package for one, and
11
+ // `ensureWasmReady` exists so a caller awaits readiness the same way whichever
12
+ // half it got.
13
+
14
+ export * from 'loro-crdt';
15
+
16
+ /** Resolve once every WASM module this package needs is ready to run. Already
17
+ * true here: a node build readies its module during import, so the callers
18
+ * that await it on the browser half wait for nothing on this one. */
19
+ export function ensureWasmReady(): Promise<void> {
20
+ return Promise.resolve();
21
+ }
@@ -0,0 +1,66 @@
1
+ // Waiting out a document's own unsaved ops.
2
+ //
3
+ // `savingCount` counts the ops this client has committed locally and the
4
+ // server has not acked, so a caller that needs the server to hold what the
5
+ // app just wrote waits on it: the import completion, and the checkpoint
6
+ // anchor, both of which describe a state rather than an intention.
7
+
8
+ import type { Document } from './documents.js';
9
+
10
+ // How often the wait re-checks. A closing document drops its subscribers
11
+ // without firing them, so this is the only thing that ends the wait for one
12
+ // that goes while writes are draining.
13
+ const SAVED_POLL_MS = 1_000;
14
+
15
+ // How long the OLDEST outstanding op may go unacked before the wait gives
16
+ // up. An op the server never received is never acked and never re-sent
17
+ // beyond the transport's own attempts, and `savingCount` then stays above
18
+ // zero for the document's life, so a wait with no bound hangs its caller
19
+ // forever. Read off the oldest op rather than off elapsed time, so a bulk
20
+ // write that takes minutes never trips it: there, every op acks promptly
21
+ // and the oldest is always young. Three forced-resend cycles wide (the
22
+ // transport forces one at 60s), so the wait outlasts the recovery it is
23
+ // waiting on.
24
+ const SAVED_STALL_MS = 180_000;
25
+
26
+ /**
27
+ * Resolve `true` once the document has no locally-issued op waiting for its
28
+ * server ack; `false` when the document is no longer live, or the acks
29
+ * stopped arriving.
30
+ *
31
+ * `false` does not say the writes were lost, only that this wait cannot say
32
+ * they landed. A caller about to record something permanent against them (an
33
+ * import completion, a checkpoint anchor) has to stop on it.
34
+ *
35
+ * @internal
36
+ */
37
+ export function whenSaved(doc: Document): Promise<boolean> {
38
+ if (doc.savingCount === 0) return Promise.resolve(true);
39
+ return new Promise<boolean>((resolve) => {
40
+ let off: (() => void) | undefined;
41
+ let poll: ReturnType<typeof setInterval> | undefined;
42
+ const settle = (saved: boolean): void => {
43
+ off?.();
44
+ if (poll !== undefined) clearInterval(poll);
45
+ resolve(saved);
46
+ };
47
+ try {
48
+ off = doc.subscribe(() => {
49
+ if (doc.savingCount === 0) settle(true);
50
+ });
51
+ } catch {
52
+ // The document closed, so nothing more is going to be acked.
53
+ resolve(false);
54
+ return;
55
+ }
56
+ poll = setInterval(() => {
57
+ if (doc.savingCount === 0) return settle(true);
58
+ if (!doc._isLive) return settle(false);
59
+ if (doc.diagnostics().oldestUnackedMs >= SAVED_STALL_MS) settle(false);
60
+ }, SAVED_POLL_MS);
61
+ // Node keeps the process alive for a pending interval; this one is
62
+ // bookkeeping for a wait that ends on its own, so it must not.
63
+ (poll as { unref?: () => void }).unref?.();
64
+ if (doc.savingCount === 0) settle(true);
65
+ });
66
+ }
package/src/ws-client.ts CHANGED
@@ -17,7 +17,11 @@
17
17
  // 10-minute idle timeout (only client-originated frames do), and it is the
18
18
  // presence liveness signal (the server refreshes `last_seen` on each ping;
19
19
  // the staleness threshold is 3 missed heartbeats). The server replies
20
- // `pong`. A topic-less `{ type: 'error', reason: 'bad_message' }` is the
20
+ // `pong`. The same interval notices a socket that has stopped carrying
21
+ // anything at all (`SOCKET_SILENCE_LIMIT_MS`), since on an idle connection
22
+ // it is the only thing running; such a socket is retired here so the
23
+ // reconnect loop can replace it, because a dead path does not reliably close
24
+ // itself. A topic-less `{ type: 'error', reason: 'bad_message' }` is the
21
25
  // server's catch-all rejection, so it is swallowed rather than surfaced (a
22
26
  // deploy without the `ping` handler answers a heartbeat with it) but never
23
27
  // counted as the connection working. Both are dropped.
@@ -103,6 +107,29 @@ const HEARTBEAT_INTERVAL_MS = 60_000;
103
107
  // rather than wrong: it parks, and returning to it clears the count. A
104
108
  // hidden page doing WORK is unaffected, because its ops draw `op_ack`.
105
109
  const HEARTBEAT_FIRST_MS = 10_000;
110
+ // How long a socket may carry nothing at all before this client treats it as
111
+ // gone. A dead path does not always close: when a phone changes network
112
+ // neither end sends a FIN, so a socket that will never carry another byte
113
+ // still reads as OPEN, and nothing downstream notices, because the reconnect
114
+ // loop is driven by `close` and an op's resend goes out on the same socket.
115
+ // ANY inbound frame answers this, not just the `pong` the reconnect count
116
+ // credits: the question is whether the path still carries bytes. Two missed
117
+ // replies plus slack, so one dropped `pong` is not enough.
118
+ const SOCKET_SILENCE_LIMIT_MS = 150_000;
119
+ // Spread added to that limit, because the condition is SHARED: the server
120
+ // answers a `ping` after an awaited presence refresh and reap sweep, so its
121
+ // replies slow under exactly the load that makes sockets fragile, and every
122
+ // client goes silent at once. An unjittered threshold turns one degradation
123
+ // into a synchronized wave of reconnects landing on a service already
124
+ // shedding.
125
+ //
126
+ // Drawn PER SOCKET, not once per client. Clients that retire together
127
+ // reconnect together, so their next silence window is tighter than the last;
128
+ // a fixed per-client offset keeps its phase inside that convergence, where a
129
+ // fresh draw on every open scatters the schedule the wave just aligned. Two
130
+ // heartbeat intervals wide, since the check rides the 60s tick and a spread
131
+ // under one tick is quantized away.
132
+ const SOCKET_SILENCE_JITTER_MS = 120_000;
106
133
  // Cap on the frames held for one topic's unacked subscribe, matching the
107
134
  // `outboundQueue` bound. A disconnect rolls every wanted topic back to
108
135
  // `subscribing`, so both queues hold the same kind of mid-reconnect frame
@@ -218,6 +245,14 @@ export class BrassWsClient {
218
245
  // so a second outage after a recovery would go unreported for the life of
219
246
  // the process.
220
247
  private creditsThisSocket = 0;
248
+ // When this socket last carried anything inbound, set on open so a socket
249
+ // that never speaks is measured from the moment it opened. Zero while no
250
+ // socket is up, which stops a silence check firing against a connection
251
+ // that does not exist.
252
+ private lastInboundAt = 0;
253
+ // This socket's silence threshold, redrawn on every open. Held as state so
254
+ // one socket cannot move its own deadline every time the check looks at it.
255
+ private silenceLimitMs = SOCKET_SILENCE_LIMIT_MS;
221
256
  private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
222
257
  // Sockets this client has opened. Every topic and every open document
223
258
  // rides this one socket, so the count is a fact about the client rather
@@ -617,6 +652,9 @@ export class BrassWsClient {
617
652
  this.connecting = false;
618
653
  this.socketsOpened += 1;
619
654
  this.creditsThisSocket = 0;
655
+ this.lastInboundAt = Date.now();
656
+ this.silenceLimitMs =
657
+ SOCKET_SILENCE_LIMIT_MS + Math.random() * SOCKET_SILENCE_JITTER_MS;
620
658
  // `socket_open` per topic: re-subscribes every wanted topic (in
621
659
  // subscription order) with a fresh retry grant, and prunes `tornDown`
622
660
  // leftovers. Runs before the outbound drain so the server sees the
@@ -649,6 +687,9 @@ export class BrassWsClient {
649
687
  }
650
688
 
651
689
  private onMessage(ev: MessageEvent): void {
690
+ // Stamped before the parse: garbage the server sent is still the server
691
+ // reaching us, and the path carrying bytes is the whole question.
692
+ this.lastInboundAt = Date.now();
652
693
  let frame: unknown;
653
694
  try {
654
695
  frame = JSON.parse(typeof ev.data === 'string' ? ev.data : String(ev.data));
@@ -757,6 +798,7 @@ export class BrassWsClient {
757
798
  private onClose(): void {
758
799
  this.stopHeartbeat();
759
800
  this.ws = null;
801
+ this.lastInboundAt = 0;
760
802
  // Held frames are tied to the connection's unacked subscribes;
761
803
  // `socket_closed` drops them and preserves wantedness for the replay.
762
804
  for (const topic of this.topics.keys()) {
@@ -917,6 +959,12 @@ export class BrassWsClient {
917
959
  // from the cadence without a second timer to keep in step.
918
960
  const arm = (delay: number): void => {
919
961
  this.heartbeatTimer = setTimeout(() => {
962
+ // The one thing running on an idle connection, so it is also what
963
+ // notices the socket stopped carrying anything.
964
+ if (this.socketHasGoneSilent()) {
965
+ this.dropSilentSocket();
966
+ return;
967
+ }
920
968
  this.sendIfOpen({ type: 'ping' });
921
969
  arm(HEARTBEAT_INTERVAL_MS);
922
970
  }, delay);
@@ -924,6 +972,27 @@ export class BrassWsClient {
924
972
  arm(HEARTBEAT_FIRST_MS);
925
973
  }
926
974
 
975
+ private socketHasGoneSilent(): boolean {
976
+ if (!this.ws || this.lastInboundAt === 0) return false;
977
+ return Date.now() - this.lastInboundAt >= this.silenceLimitMs;
978
+ }
979
+
980
+ // Retire a socket the server has stopped answering on. `onClose` runs
981
+ // directly rather than being waited for: closing a dead path has nothing to
982
+ // complete the handshake with, so the socket can sit in CLOSING and never
983
+ // fire `close`, which is the state this exists to escape. Running the
984
+ // teardown first also lets the identity guard drop a late `close`, so the
985
+ // reconnect is scheduled once.
986
+ private dropSilentSocket(): void {
987
+ const dead = this.ws;
988
+ this.onClose();
989
+ try {
990
+ dead?.close();
991
+ } catch {
992
+ // Closing an already-dead socket is fine; the teardown already ran.
993
+ }
994
+ }
995
+
927
996
  private stopHeartbeat(): void {
928
997
  if (this.heartbeatTimer) {
929
998
  clearTimeout(this.heartbeatTimer);
@@ -1,18 +1,17 @@
1
1
  // The ceilings an op can hit, measured before it is sent.
2
- //
3
- // Duplicated from the server's `frameExceedsWsLimit` / `op-blob.ts`
4
- // rather than imported: this package ships unbundled and can only import
5
- // its own relative files. The server owns the contract; these follow it.
6
2
 
7
- // A frame over this closes the connection, taking every other op and
8
- // presence down with it, and the reconnect re-sends the oversized frame, so
9
- // the document never saves anything. The transport also caps a message at
10
- // 128 KiB, but the frame binds first in a browser: it negotiates
3
+ // The client's own SEND bound, deliberately smaller than the platform's
4
+ // 128 KiB server-push bound: the two are different limits, not one shared
5
+ // constant. A frame over this closes the connection, taking every other op
6
+ // and presence down with it, and the reconnect re-sends the oversized
7
+ // frame, so the document never saves anything. The transport also caps a
8
+ // message at 128 KiB, but the frame binds first in a browser: it negotiates
11
9
  // `permessage-deflate` and Chromium sends a message as one frame, so what
12
10
  // gets through depends on how the op compresses (43,264 bytes for
13
11
  // incompressible base64, measured). Staying at the frame limit is what makes
14
12
  // the bound independent of the content.
15
- const WS_FRAME_MAX_BYTES = 32 * 1024;
13
+ /** @internal Exported so a test can pin it against the server's bound. */
14
+ export const WS_SEND_FRAME_MAX_BYTES = 32 * 1024;
16
15
 
17
16
  // Past this the blob is uploaded to storage and the write names the staged
18
17
  // object instead of carrying it. Set by what a request body delivers
@@ -43,7 +42,7 @@ export interface OpFrame {
43
42
  */
44
43
  export function opFrameExceedsWsLimit(frame: OpFrame): boolean {
45
44
  const envelopeChars = JSON.stringify({ ...frame, op_blob: '' }).length;
46
- return envelopeChars + frame.op_blob.length > WS_FRAME_MAX_BYTES;
45
+ return envelopeChars + frame.op_blob.length > WS_SEND_FRAME_MAX_BYTES;
47
46
  }
48
47
 
49
48
  /** True when the blob is too large to ride in a request body, so it is
package/dist/vite.d.ts DELETED
@@ -1,31 +0,0 @@
1
- interface ViteAliasEntry {
2
- find: string | RegExp;
3
- replacement: string;
4
- }
5
- interface ViteBuildOptions {
6
- target?: string | string[] | false;
7
- }
8
- interface ViteUserConfig {
9
- build?: ViteBuildOptions;
10
- environments?: {
11
- client?: {
12
- build?: ViteBuildOptions;
13
- };
14
- };
15
- resolve?: {
16
- alias?: Record<string, string> | readonly ViteAliasEntry[];
17
- };
18
- optimizeDeps?: {
19
- exclude?: string[];
20
- esbuildOptions?: {
21
- target?: string | string[];
22
- };
23
- };
24
- }
25
- interface VitePlugin {
26
- name: string;
27
- config(config: ViteUserConfig): Promise<ViteUserConfig>;
28
- }
29
- export declare function brassVite(): VitePlugin;
30
- export {};
31
- //# sourceMappingURL=vite.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"vite.d.ts","sourceRoot":"","sources":["../src/vite.ts"],"names":[],"mappings":"AA+CA,UAAU,cAAc;IACtB,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IACtB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,UAAU,gBAAgB;IACxB,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,KAAK,CAAC;CACpC;AAED,UAAU,cAAc;IACtB,KAAK,CAAC,EAAE,gBAAgB,CAAC;IAGzB,YAAY,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE;YAAE,KAAK,CAAC,EAAE,gBAAgB,CAAA;SAAE,CAAA;KAAE,CAAC;IACzD,OAAO,CAAC,EAAE;QACR,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,cAAc,EAAE,CAAC;KAC5D,CAAC;IACF,YAAY,CAAC,EAAE;QACb,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;QACnB,cAAc,CAAC,EAAE;YAAE,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAA;SAAE,CAAC;KACjD,CAAC;CACH;AAED,UAAU,UAAU;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,MAAM,EAAE,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC;CACzD;AAoGD,wBAAgB,SAAS,IAAI,UAAU,CAwCtC"}
package/dist/vite.js DELETED
@@ -1,161 +0,0 @@
1
- // Vite plugin encapsulating the bundler config the SDK's WASM runtime
2
- // needs, so users never name the underlying WASM dep in their own
3
- // `vite.config.ts`.
4
- //
5
- // Vite's dep-prebundling targets older browsers by default and fails on the
6
- // WASM dep's top-level await (`Top-level await is not available in the
7
- // configured target environment for loro-crdt/bundler/loro_wasm.js`). The
8
- // plugin excludes the dep from prebundling (leaving it for the build-time
9
- // WASM loaders) and bumps the prebundle target to es2022.
10
- //
11
- // `vite-plugin-wasm` remains the caller's responsibility: it must land before
12
- // `brassVite()` in the plugin list.
13
- // Every WASM-shipping dep the SDK pulls in, and the entry that loads it
14
- // asynchronously. Add an entry rather than replacing the list: a consumer who
15
- // has two installed during a migration window needs both handled, and this is
16
- // the one place any of them is named, so an app's own config never is.
17
- //
18
- // A default browser entry fetches its module with a SYNCHRONOUS
19
- // XMLHttpRequest while it evaluates, so an app freezes its main thread for the
20
- // whole download before it paints, whether or not anyone opens a document, and
21
- // the freeze grows with the reader's latency rather than with the app. An
22
- // `asyncEntry` hands the same module to the bundler's own asset pipeline,
23
- // which loads it asynchronously and leaves the main thread free. Selecting one
24
- // emits a top-level await, so it is reachable only on a build target that
25
- // accepts one, and the synchronous entry is what keeps every other target
26
- // building.
27
- const WASM_DEPS = [
28
- { specifier: 'loro-crdt', asyncEntry: 'loro-crdt/bundler' },
29
- ];
30
- // A specifier matched whole. A string `find` matches by prefix, which would
31
- // also catch the async entry the alias points AT and resolve it to itself.
32
- const wholeSpecifier = (specifier) => new RegExp(`^${specifier.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`);
33
- // Vite 8 prebundles with rolldown, which handles top-level await on its own
34
- // and deprecates `optimizeDeps.esbuildOptions`. Setting it there does nothing
35
- // except print a deprecation warning naming this plugin, on every `vite dev`
36
- // and every `vite build` a consumer runs. Read from the running Vite rather
37
- // than from the consumer's manifest, and treat an unreadable version as an
38
- // older Vite, where the target bump is what makes the WASM dep load at all.
39
- //
40
- // The specifier is held in a variable so `tsc` does not resolve it. Vite is
41
- // neither a dependency nor a peer dependency here (the config shapes above are
42
- // declared for the same reason), so a literal specifier fails the build
43
- // wherever Vite is absent, which includes `npm install` in this package
44
- // itself. At runtime the import reaches the Vite running the build.
45
- const VITE_PACKAGE = 'vite';
46
- async function viteMajor() {
47
- try {
48
- const { version } = (await import(VITE_PACKAGE));
49
- const major = Number.parseInt(version ?? '', 10);
50
- return Number.isFinite(major) ? major : null;
51
- }
52
- catch {
53
- return null;
54
- }
55
- }
56
- // Browser versions that first shipped top-level await, by the engine names
57
- // esbuild accepts in a target string. A target naming an engine absent here is
58
- // read as unsupported.
59
- const TOP_LEVEL_AWAIT_SINCE = {
60
- chrome: 89,
61
- edge: 89,
62
- firefox: 89,
63
- safari: 15,
64
- ios: 15,
65
- opera: 75,
66
- node: 14.8,
67
- es: 2022,
68
- hermes: Number.POSITIVE_INFINITY,
69
- ie: Number.POSITIVE_INFINITY,
70
- };
71
- // Vite's own named targets. `modules` (the Vite 5 / 6 default) resolves to
72
- // browsers predating top-level await; `baseline-widely-available` (the Vite 7 /
73
- // 8 default) resolves to ones that carry it.
74
- const NAMED_TARGET_SUPPORT = {
75
- esnext: true,
76
- 'baseline-widely-available': true,
77
- modules: false,
78
- };
79
- function targetSupportsTopLevelAwait(target) {
80
- const t = target.trim().toLowerCase();
81
- const named = NAMED_TARGET_SUPPORT[t];
82
- if (named !== undefined)
83
- return named;
84
- const parsed = /^([a-z]+)([\d.]+)$/.exec(t);
85
- if (!parsed)
86
- return false;
87
- const since = TOP_LEVEL_AWAIT_SINCE[parsed[1]];
88
- if (since === undefined)
89
- return false;
90
- const version = Number.parseFloat(parsed[2]);
91
- return Number.isFinite(version) && version >= since;
92
- }
93
- // Whether the build this plugin is configuring can carry a top-level await.
94
- // An unreadable or unrecognised target answers `false`, so the worst a wrong
95
- // reading costs is the synchronous entry a consumer already has.
96
- function buildAcceptsTopLevelAwait(config, major) {
97
- const target = config.environments?.client?.build?.target ?? config.build?.target;
98
- // No explicit target: the answer is Vite's own default, which carries
99
- // top-level await from Vite 7 (`baseline-widely-available`) and not before
100
- // (`modules`).
101
- if (target === undefined)
102
- return major !== null && major >= 7;
103
- if (target === false)
104
- return false;
105
- const targets = Array.isArray(target) ? target : [target];
106
- return targets.length > 0 && targets.every(targetSupportsTopLevelAwait);
107
- }
108
- // A consumer who already resolves the WASM dep themselves owns that decision,
109
- // so the plugin adds nothing that would take it over.
110
- function aliasesWasmDep(config) {
111
- const alias = config.resolve?.alias;
112
- if (!alias)
113
- return false;
114
- const specifiers = WASM_DEPS.map((dep) => dep.specifier);
115
- if (Array.isArray(alias)) {
116
- return alias.some((entry) => specifiers.some((specifier) => typeof entry.find === 'string'
117
- ? entry.find === specifier
118
- : entry.find.test(specifier)));
119
- }
120
- return Object.keys(alias).some((key) => specifiers.includes(key));
121
- }
122
- export function brassVite() {
123
- return {
124
- name: 'brass-build:vite',
125
- async config(config) {
126
- const major = await viteMajor();
127
- const useAsyncEntry = buildAcceptsTopLevelAwait(config, major) && !aliasesWasmDep(config);
128
- const existingExclude = config.optimizeDeps?.exclude ?? [];
129
- const exclude = Array.from(new Set([
130
- ...existingExclude,
131
- ...WASM_DEPS.map((dep) => dep.specifier),
132
- ...(useAsyncEntry ? WASM_DEPS.map((dep) => dep.asyncEntry) : []),
133
- ]));
134
- const resolve = useAsyncEntry
135
- ? {
136
- resolve: {
137
- alias: WASM_DEPS.map((dep) => ({
138
- find: wholeSpecifier(dep.specifier),
139
- replacement: dep.asyncEntry,
140
- })),
141
- },
142
- }
143
- : {};
144
- if (major !== null && major >= 8) {
145
- return { ...resolve, optimizeDeps: { exclude } };
146
- }
147
- const existingTarget = config.optimizeDeps?.esbuildOptions?.target;
148
- // Don't downgrade if a consumer has already pinned a newer
149
- // target; only raise from the Vite default (which lacks TLA).
150
- const target = existingTarget ?? 'es2022';
151
- return {
152
- ...resolve,
153
- optimizeDeps: {
154
- exclude,
155
- esbuildOptions: { target },
156
- },
157
- };
158
- },
159
- };
160
- }
161
- //# sourceMappingURL=vite.js.map
package/dist/vite.js.map DELETED
@@ -1 +0,0 @@
1
- {"version":3,"file":"vite.js","sourceRoot":"","sources":["../src/vite.ts"],"names":[],"mappings":"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"}