spacetimedb 2.1.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/LICENSE.txt +2 -2
  2. package/dist/angular/index.cjs +7 -2
  3. package/dist/angular/index.cjs.map +1 -1
  4. package/dist/angular/index.mjs +7 -2
  5. package/dist/angular/index.mjs.map +1 -1
  6. package/dist/browser/angular/index.mjs +7 -2
  7. package/dist/browser/angular/index.mjs.map +1 -1
  8. package/dist/browser/react/index.mjs +57 -6
  9. package/dist/browser/react/index.mjs.map +1 -1
  10. package/dist/browser/svelte/index.mjs +7 -2
  11. package/dist/browser/svelte/index.mjs.map +1 -1
  12. package/dist/browser/vue/index.mjs +7 -2
  13. package/dist/browser/vue/index.mjs.map +1 -1
  14. package/dist/index.browser.mjs +459 -138
  15. package/dist/index.browser.mjs.map +1 -1
  16. package/dist/index.cjs +459 -138
  17. package/dist/index.cjs.map +1 -1
  18. package/dist/index.mjs +459 -138
  19. package/dist/index.mjs.map +1 -1
  20. package/dist/lib/binary_reader.d.ts +1 -1
  21. package/dist/lib/binary_reader.d.ts.map +1 -1
  22. package/dist/lib/binary_writer.d.ts +2 -1
  23. package/dist/lib/binary_writer.d.ts.map +1 -1
  24. package/dist/lib/filter.d.ts +2 -1
  25. package/dist/lib/filter.d.ts.map +1 -1
  26. package/dist/lib/table.d.ts +6 -0
  27. package/dist/lib/table.d.ts.map +1 -1
  28. package/dist/min/index.browser.mjs +1 -1
  29. package/dist/min/index.browser.mjs.map +1 -1
  30. package/dist/min/react/index.mjs +1 -1
  31. package/dist/min/react/index.mjs.map +1 -1
  32. package/dist/min/sdk/index.browser.mjs +1 -1
  33. package/dist/min/sdk/index.browser.mjs.map +1 -1
  34. package/dist/react/index.cjs +57 -5
  35. package/dist/react/index.cjs.map +1 -1
  36. package/dist/react/index.d.ts +1 -0
  37. package/dist/react/index.d.ts.map +1 -1
  38. package/dist/react/index.mjs +57 -6
  39. package/dist/react/index.mjs.map +1 -1
  40. package/dist/react/useProcedure.d.ts +4 -0
  41. package/dist/react/useProcedure.d.ts.map +1 -0
  42. package/dist/react/useTable.d.ts +2 -0
  43. package/dist/react/useTable.d.ts.map +1 -1
  44. package/dist/sdk/db_connection_builder.d.ts +3 -3
  45. package/dist/sdk/db_connection_builder.d.ts.map +1 -1
  46. package/dist/sdk/db_connection_impl.d.ts +3 -3
  47. package/dist/sdk/db_connection_impl.d.ts.map +1 -1
  48. package/dist/sdk/decompress.d.ts +1 -1
  49. package/dist/sdk/decompress.d.ts.map +1 -1
  50. package/dist/sdk/index.browser.mjs +459 -138
  51. package/dist/sdk/index.browser.mjs.map +1 -1
  52. package/dist/sdk/index.cjs +459 -138
  53. package/dist/sdk/index.cjs.map +1 -1
  54. package/dist/sdk/index.mjs +459 -138
  55. package/dist/sdk/index.mjs.map +1 -1
  56. package/dist/sdk/table_cache.d.ts +1 -0
  57. package/dist/sdk/table_cache.d.ts.map +1 -1
  58. package/dist/sdk/type_utils.d.ts +4 -1
  59. package/dist/sdk/type_utils.d.ts.map +1 -1
  60. package/dist/sdk/websocket_decompress_adapter.d.ts +5 -21
  61. package/dist/sdk/websocket_decompress_adapter.d.ts.map +1 -1
  62. package/dist/sdk/websocket_protocols.d.ts +6 -0
  63. package/dist/sdk/websocket_protocols.d.ts.map +1 -0
  64. package/dist/sdk/websocket_test_adapter.d.ts +14 -18
  65. package/dist/sdk/websocket_test_adapter.d.ts.map +1 -1
  66. package/dist/sdk/websocket_v3_frames.d.ts +9 -0
  67. package/dist/sdk/websocket_v3_frames.d.ts.map +1 -0
  68. package/dist/sdk/ws.d.ts +26 -1
  69. package/dist/sdk/ws.d.ts.map +1 -1
  70. package/dist/server/http_internal.d.ts.map +1 -1
  71. package/dist/server/index.d.ts +1 -1
  72. package/dist/server/index.d.ts.map +1 -1
  73. package/dist/server/index.mjs +53 -6
  74. package/dist/server/index.mjs.map +1 -1
  75. package/dist/server/runtime.d.ts +29 -2
  76. package/dist/server/runtime.d.ts.map +1 -1
  77. package/dist/svelte/index.cjs +7 -2
  78. package/dist/svelte/index.cjs.map +1 -1
  79. package/dist/svelte/index.mjs +7 -2
  80. package/dist/svelte/index.mjs.map +1 -1
  81. package/dist/tanstack/index.cjs +7 -2
  82. package/dist/tanstack/index.cjs.map +1 -1
  83. package/dist/tanstack/index.mjs +7 -2
  84. package/dist/tanstack/index.mjs.map +1 -1
  85. package/dist/vue/index.cjs +7 -2
  86. package/dist/vue/index.cjs.map +1 -1
  87. package/dist/vue/index.mjs +7 -2
  88. package/dist/vue/index.mjs.map +1 -1
  89. package/package.json +2 -2
  90. package/src/lib/binary_reader.ts +5 -2
  91. package/src/lib/binary_writer.ts +7 -1
  92. package/src/lib/filter.ts +12 -1
  93. package/src/lib/table.ts +9 -1
  94. package/src/react/index.ts +1 -0
  95. package/src/react/useProcedure.ts +60 -0
  96. package/src/react/useTable.ts +17 -2
  97. package/src/sdk/db_connection_builder.ts +16 -7
  98. package/src/sdk/db_connection_impl.ts +404 -89
  99. package/src/sdk/decompress.ts +7 -23
  100. package/src/sdk/table_cache.ts +5 -5
  101. package/src/sdk/type_utils.ts +10 -1
  102. package/src/sdk/websocket_decompress_adapter.ts +15 -77
  103. package/src/sdk/websocket_protocols.ts +25 -0
  104. package/src/sdk/websocket_test_adapter.ts +65 -29
  105. package/src/sdk/websocket_v3_frames.ts +126 -0
  106. package/src/sdk/ws.ts +81 -3
  107. package/src/server/http_internal.ts +10 -1
  108. package/src/server/index.ts +1 -1
  109. package/src/server/runtime.ts +39 -1
  110. package/src/server/sys.d.ts +4 -0
package/dist/index.cjs CHANGED
@@ -430,8 +430,8 @@ var BinaryReader = class {
430
430
  this.view = input instanceof DataView ? input : new DataView(input.buffer, input.byteOffset, input.byteLength);
431
431
  this.offset = 0;
432
432
  }
433
- reset(view) {
434
- this.view = view;
433
+ reset(input) {
434
+ this.view = input instanceof DataView ? input : new DataView(input.buffer, input.byteOffset, input.byteLength);
435
435
  this.offset = 0;
436
436
  }
437
437
  get remaining() {
@@ -623,6 +623,11 @@ var BinaryWriter = class {
623
623
  this.view.setUint8(this.offset, value);
624
624
  this.offset += 1;
625
625
  }
626
+ writeBytes(value) {
627
+ this.expandBuffer(value.length);
628
+ new Uint8Array(this.buffer.buffer, this.offset, value.length).set(value);
629
+ this.offset += value.length;
630
+ }
626
631
  writeI8(value) {
627
632
  this.expandBuffer(1);
628
633
  this.view.setInt8(this.offset, value);
@@ -4467,6 +4472,7 @@ var scalarCompare = (x, y) => {
4467
4472
  return x < y ? -1 : 1;
4468
4473
  };
4469
4474
  var TableCacheImpl = class {
4475
+ hasPrimaryKey;
4470
4476
  rows;
4471
4477
  tableDef;
4472
4478
  emitter;
@@ -4480,6 +4486,9 @@ var TableCacheImpl = class {
4480
4486
  this.tableDef = tableDef;
4481
4487
  this.rows = /* @__PURE__ */ new Map();
4482
4488
  this.emitter = new EventEmitter();
4489
+ this.hasPrimaryKey = Object.values(this.tableDef.columns).some(
4490
+ (col) => col.columnMetadata.isPrimaryKey === true
4491
+ );
4483
4492
  for (const idxDef of this.tableDef.resolvedIndexes) {
4484
4493
  const index = this.#makeReadonlyIndex(this.tableDef, idxDef);
4485
4494
  this[idxDef.name] = index;
@@ -4589,10 +4598,7 @@ var TableCacheImpl = class {
4589
4598
  }
4590
4599
  return pendingCallbacks;
4591
4600
  }
4592
- const hasPrimaryKey = Object.values(this.tableDef.columns).some(
4593
- (col) => col.columnMetadata.isPrimaryKey === true
4594
- );
4595
- if (hasPrimaryKey) {
4601
+ if (this.hasPrimaryKey) {
4596
4602
  const insertMap = /* @__PURE__ */ new Map();
4597
4603
  const deleteMap = /* @__PURE__ */ new Map();
4598
4604
  for (const op of operations) {
@@ -5000,27 +5006,17 @@ async function decompress(buffer, type, chunkSize = 128 * 1024) {
5000
5006
  });
5001
5007
  const decompressionStream = new DecompressionStream(type);
5002
5008
  const decompressedStream = readableStream.pipeThrough(decompressionStream);
5003
- const reader = decompressedStream.getReader();
5004
5009
  const chunks = [];
5005
- let totalLength = 0;
5006
- let result;
5007
- while (!(result = await reader.read()).done) {
5008
- chunks.push(result.value);
5009
- totalLength += result.value.length;
5010
- }
5011
- const decompressedArray = new Uint8Array(totalLength);
5012
- let chunkOffset = 0;
5013
- for (const chunk of chunks) {
5014
- decompressedArray.set(chunk, chunkOffset);
5015
- chunkOffset += chunk.length;
5016
- }
5017
- return decompressedArray;
5010
+ for await (const chunk of decompressedStream) {
5011
+ chunks.push(chunk);
5012
+ }
5013
+ return new Blob(chunks).bytes();
5018
5014
  }
5019
5015
 
5020
5016
  // src/sdk/ws.ts
5021
5017
  async function resolveWS() {
5022
- if (typeof globalThis.WebSocket !== "undefined") {
5023
- return globalThis.WebSocket;
5018
+ if (typeof WebSocket !== "undefined") {
5019
+ return WebSocket;
5024
5020
  }
5025
5021
  const dynamicImport = new Function("m", "return import(m)");
5026
5022
  try {
@@ -5034,9 +5030,54 @@ async function resolveWS() {
5034
5030
  throw err;
5035
5031
  }
5036
5032
  }
5033
+ async function openWebSocket({
5034
+ url,
5035
+ nameOrAddress,
5036
+ wsProtocol,
5037
+ authToken,
5038
+ compression,
5039
+ lightMode,
5040
+ confirmedReads
5041
+ }) {
5042
+ const headers = new Headers();
5043
+ const WS = await resolveWS();
5044
+ let temporaryAuthToken;
5045
+ if (authToken) {
5046
+ headers.set("Authorization", `Bearer ${authToken}`);
5047
+ const tokenUrl = new URL("v1/identity/websocket-token", url);
5048
+ tokenUrl.protocol = url.protocol === "wss:" ? "https:" : "http:";
5049
+ const response = await fetch(tokenUrl, { method: "POST", headers });
5050
+ if (response.ok) {
5051
+ const { token } = await response.json();
5052
+ temporaryAuthToken = token;
5053
+ } else {
5054
+ throw new Error(`Failed to verify token: ${response.statusText}`);
5055
+ }
5056
+ }
5057
+ const databaseUrl = new URL(`v1/database/${nameOrAddress}/subscribe`, url);
5058
+ if (temporaryAuthToken) {
5059
+ databaseUrl.searchParams.set("token", temporaryAuthToken);
5060
+ }
5061
+ databaseUrl.searchParams.set(
5062
+ "compression",
5063
+ { gzip: "Gzip", brotli: "Brotli", none: "None" }[compression] ?? "None"
5064
+ );
5065
+ if (lightMode) {
5066
+ databaseUrl.searchParams.set("light", "true");
5067
+ }
5068
+ if (confirmedReads !== void 0) {
5069
+ databaseUrl.searchParams.set("confirmed", confirmedReads.toString());
5070
+ }
5071
+ const ws = new WS(databaseUrl.toString(), wsProtocol);
5072
+ ws.binaryType = "arraybuffer";
5073
+ return ws;
5074
+ }
5037
5075
 
5038
5076
  // src/sdk/websocket_decompress_adapter.ts
5039
5077
  var WebsocketDecompressAdapter = class _WebsocketDecompressAdapter {
5078
+ get protocol() {
5079
+ return this.#ws.protocol;
5080
+ }
5040
5081
  set onclose(handler) {
5041
5082
  this.#ws.onclose = handler;
5042
5083
  }
@@ -5060,9 +5101,7 @@ var WebsocketDecompressAdapter = class _WebsocketDecompressAdapter {
5060
5101
  case 0:
5061
5102
  return data;
5062
5103
  case 1:
5063
- throw new Error(
5064
- "Brotli Compression not supported. Please use gzip or none compression in withCompression method on DbConnection."
5065
- );
5104
+ return await decompress(data, "brotli");
5066
5105
  case 2:
5067
5106
  return await decompress(data, "gzip");
5068
5107
  default:
@@ -5078,51 +5117,10 @@ var WebsocketDecompressAdapter = class _WebsocketDecompressAdapter {
5078
5117
  this.#ws.close();
5079
5118
  }
5080
5119
  constructor(ws) {
5081
- ws.binaryType = "arraybuffer";
5082
5120
  this.#ws = ws;
5083
5121
  }
5084
- static async createWebSocketFn({
5085
- url,
5086
- nameOrAddress,
5087
- wsProtocol,
5088
- authToken,
5089
- compression,
5090
- lightMode,
5091
- confirmedReads
5092
- }) {
5093
- const headers = new Headers();
5094
- const WS = await resolveWS();
5095
- let temporaryAuthToken = void 0;
5096
- if (authToken) {
5097
- headers.set("Authorization", `Bearer ${authToken}`);
5098
- const tokenUrl = new URL("v1/identity/websocket-token", url);
5099
- tokenUrl.protocol = url.protocol === "wss:" ? "https:" : "http:";
5100
- const response = await fetch(tokenUrl, { method: "POST", headers });
5101
- if (response.ok) {
5102
- const { token } = await response.json();
5103
- temporaryAuthToken = token;
5104
- } else {
5105
- return Promise.reject(
5106
- new Error(`Failed to verify token: ${response.statusText}`)
5107
- );
5108
- }
5109
- }
5110
- const databaseUrl = new URL(`v1/database/${nameOrAddress}/subscribe`, url);
5111
- if (temporaryAuthToken) {
5112
- databaseUrl.searchParams.set("token", temporaryAuthToken);
5113
- }
5114
- databaseUrl.searchParams.set(
5115
- "compression",
5116
- compression === "gzip" ? "Gzip" : "None"
5117
- );
5118
- if (lightMode) {
5119
- databaseUrl.searchParams.set("light", "true");
5120
- }
5121
- if (confirmedReads !== void 0) {
5122
- databaseUrl.searchParams.set("confirmed", confirmedReads.toString());
5123
- }
5124
- const ws = new WS(databaseUrl.toString(), wsProtocol);
5125
- return new _WebsocketDecompressAdapter(ws);
5122
+ static async openWebSocket(args) {
5123
+ return new _WebsocketDecompressAdapter(await openWebSocket(args));
5126
5124
  }
5127
5125
  };
5128
5126
 
@@ -5139,7 +5137,7 @@ var DbConnectionBuilder = class {
5139
5137
  constructor(remoteModule, dbConnectionCtor) {
5140
5138
  this.remoteModule = remoteModule;
5141
5139
  this.dbConnectionCtor = dbConnectionCtor;
5142
- this.#createWSFn = WebsocketDecompressAdapter.createWebSocketFn;
5140
+ this.#createWSFn = WebsocketDecompressAdapter.openWebSocket;
5143
5141
  }
5144
5142
  #uri;
5145
5143
  #nameOrAddress;
@@ -5194,6 +5192,16 @@ var DbConnectionBuilder = class {
5194
5192
  * @param compression The compression algorithm to use for the connection.
5195
5193
  */
5196
5194
  withCompression(compression) {
5195
+ if (compression === "brotli") {
5196
+ try {
5197
+ new DecompressionStream("brotli");
5198
+ } catch (e) {
5199
+ throw new TypeError(
5200
+ `Brotli compression is not supported by the runtime. Please choose a different compression method.`,
5201
+ { cause: e }
5202
+ );
5203
+ }
5204
+ }
5197
5205
  this.#compression = compression;
5198
5206
  return this;
5199
5207
  }
@@ -5557,6 +5565,100 @@ var SubscriptionHandleImpl = class {
5557
5565
  return this.#activeState;
5558
5566
  }
5559
5567
  };
5568
+
5569
+ // src/sdk/websocket_protocols.ts
5570
+ var V2_WS_PROTOCOL = "v2.bsatn.spacetimedb";
5571
+ var V3_WS_PROTOCOL = "v3.bsatn.spacetimedb";
5572
+ var PREFERRED_WS_PROTOCOLS = [V3_WS_PROTOCOL, V2_WS_PROTOCOL];
5573
+ function normalizeWsProtocol(protocol) {
5574
+ if (protocol === V3_WS_PROTOCOL) {
5575
+ return V3_WS_PROTOCOL;
5576
+ }
5577
+ if (protocol === "" || protocol === V2_WS_PROTOCOL) {
5578
+ return V2_WS_PROTOCOL;
5579
+ }
5580
+ stdbLogger(
5581
+ "warn",
5582
+ `Unexpected websocket subprotocol "${protocol}", falling back to ${V2_WS_PROTOCOL}.`
5583
+ );
5584
+ return V2_WS_PROTOCOL;
5585
+ }
5586
+
5587
+ // src/sdk/websocket_v3_frames.ts
5588
+ var EMPTY_V3_PAYLOAD_ERR = "v3 websocket payloads must contain at least one message";
5589
+ function ensureMessages(messages) {
5590
+ if (messages.length === 0) {
5591
+ throw new RangeError(EMPTY_V3_PAYLOAD_ERR);
5592
+ }
5593
+ }
5594
+ function ensureMessageCount(messages, messageCount) {
5595
+ ensureMessages(messages);
5596
+ if (messageCount < 1 || messageCount > messages.length) {
5597
+ throw new RangeError(
5598
+ `v3 websocket payload requested ${messageCount} messages from ${messages.length}`
5599
+ );
5600
+ }
5601
+ }
5602
+ function concatenateMessagesV3(writer, messages, messageCount = messages.length) {
5603
+ ensureMessageCount(messages, messageCount);
5604
+ writer.clear();
5605
+ for (let i = 0; i < messageCount; i++) {
5606
+ writer.writeBytes(messages[i]);
5607
+ }
5608
+ return writer.getBuffer();
5609
+ }
5610
+ function countClientMessagesForV3Frame(messages, maxFrameBytes) {
5611
+ ensureMessages(messages);
5612
+ const firstMessage = messages[0];
5613
+ if (firstMessage.length > maxFrameBytes) {
5614
+ return 1;
5615
+ }
5616
+ let count = 1;
5617
+ let frameSize = firstMessage.length;
5618
+ while (count < messages.length) {
5619
+ const nextMessage = messages[count];
5620
+ const nextFrameSize = frameSize + nextMessage.length;
5621
+ if (nextFrameSize > maxFrameBytes) {
5622
+ break;
5623
+ }
5624
+ frameSize = nextFrameSize;
5625
+ count += 1;
5626
+ }
5627
+ return count;
5628
+ }
5629
+ function encodeClientMessagesV3(writer, messages, messageCount = messages.length) {
5630
+ return concatenateMessagesV3(writer, messages, messageCount);
5631
+ }
5632
+ function forEachServerMessageV3(reader, data, visit) {
5633
+ reader.reset(data);
5634
+ if (reader.remaining === 0) {
5635
+ throw new RangeError(EMPTY_V3_PAYLOAD_ERR);
5636
+ }
5637
+ let count = 0;
5638
+ while (reader.remaining > 0) {
5639
+ visit(ServerMessage.deserialize(reader));
5640
+ count += 1;
5641
+ }
5642
+ return count;
5643
+ }
5644
+
5645
+ // src/sdk/db_connection_impl.ts
5646
+ var TEXT_ENCODER = new TextEncoder();
5647
+ function getClientMessageVariantTag(name) {
5648
+ if (ClientMessage.algebraicType.tag !== "Sum") {
5649
+ throw new TypeError("ClientMessage must be a sum type");
5650
+ }
5651
+ const tag = ClientMessage.algebraicType.value.variants.findIndex(
5652
+ (variant) => variant.name === name
5653
+ );
5654
+ if (tag === -1) {
5655
+ throw new RangeError(`Unknown ClientMessage variant: ${name}`);
5656
+ }
5657
+ return tag;
5658
+ }
5659
+ var CLIENT_MESSAGE_CALL_REDUCER_TAG = getClientMessageVariantTag("CallReducer");
5660
+ var CLIENT_MESSAGE_CALL_PROCEDURE_TAG = getClientMessageVariantTag("CallProcedure");
5661
+ var MAX_V3_OUTBOUND_FRAME_BYTES = 256 * 1024;
5560
5662
  var DbConnectionImpl = class {
5561
5663
  /**
5562
5664
  * Whether or not the connection is active.
@@ -5591,22 +5693,35 @@ var DbConnectionImpl = class {
5591
5693
  * The `ConnectionId` of the connection to to the database.
5592
5694
  */
5593
5695
  connectionId = ConnectionId.random();
5696
+ #connectionIdHex = this.connectionId.toHexString();
5594
5697
  // These fields are meant to be strictly private.
5595
5698
  #queryId = 0;
5596
5699
  #requestId = 0;
5597
5700
  #eventId = 0;
5598
5701
  #emitter;
5599
- #messageQueue = Promise.resolve();
5702
+ #inboundQueue = [];
5703
+ #inboundQueueOffset = 0;
5704
+ #isDrainingInboundQueue = false;
5600
5705
  #outboundQueue = [];
5706
+ #isOutboundFlushScheduled = false;
5707
+ #negotiatedWsProtocol = V2_WS_PROTOCOL;
5601
5708
  #subscriptionManager = new SubscriptionManager();
5602
5709
  #remoteModule;
5603
5710
  #reducerCallbacks = /* @__PURE__ */ new Map();
5604
5711
  #reducerCallInfo = /* @__PURE__ */ new Map();
5605
5712
  #procedureCallbacks = /* @__PURE__ */ new Map();
5606
5713
  #rowDeserializers;
5714
+ #rowIdMetadata;
5607
5715
  #reducerArgsSerializers;
5608
5716
  #procedureSerializers;
5717
+ #reducerNameBytes;
5718
+ #procedureNameBytes;
5609
5719
  #sourceNameToTableDef;
5720
+ #messageReader = new BinaryReader(new Uint8Array());
5721
+ #rowListReader = new BinaryReader(new Uint8Array());
5722
+ #clientFrameEncoder = new BinaryWriter(1024);
5723
+ #boundSubscriptionBuilder;
5724
+ #boundDisconnect;
5610
5725
  // These fields are not part of the public API, but in a pinch you
5611
5726
  // could use JavaScript to access them by bypassing TypeScript's
5612
5727
  // private fields.
@@ -5635,22 +5750,35 @@ var DbConnectionImpl = class {
5635
5750
  this.token = token;
5636
5751
  this.#remoteModule = remoteModule;
5637
5752
  this.#emitter = emitter;
5753
+ this.#boundSubscriptionBuilder = this.subscriptionBuilder.bind(this);
5754
+ this.#boundDisconnect = this.disconnect.bind(this);
5638
5755
  this.#rowDeserializers = /* @__PURE__ */ Object.create(null);
5756
+ this.#rowIdMetadata = /* @__PURE__ */ Object.create(null);
5639
5757
  this.#sourceNameToTableDef = /* @__PURE__ */ Object.create(null);
5640
5758
  for (const table2 of Object.values(remoteModule.tables)) {
5641
5759
  this.#rowDeserializers[table2.sourceName] = ProductType.makeDeserializer(
5642
5760
  table2.rowType
5643
5761
  );
5644
5762
  this.#sourceNameToTableDef[table2.sourceName] = table2;
5763
+ const primaryKeyColumn = Object.entries(table2.columns).find(
5764
+ ([, column]) => column.columnMetadata.isPrimaryKey
5765
+ );
5766
+ this.#rowIdMetadata[table2.sourceName] = primaryKeyColumn ? {
5767
+ primaryKeyColName: primaryKeyColumn[0],
5768
+ primaryKeyColType: primaryKeyColumn[1].typeBuilder.algebraicType
5769
+ } : {};
5645
5770
  }
5646
5771
  this.#reducerArgsSerializers = /* @__PURE__ */ Object.create(null);
5772
+ this.#reducerNameBytes = /* @__PURE__ */ Object.create(null);
5647
5773
  for (const reducer of remoteModule.reducers) {
5648
5774
  this.#reducerArgsSerializers[reducer.name] = {
5649
5775
  serialize: ProductType.makeSerializer(reducer.paramsType),
5650
5776
  deserialize: ProductType.makeDeserializer(reducer.paramsType)
5651
5777
  };
5778
+ this.#reducerNameBytes[reducer.name] = TEXT_ENCODER.encode(reducer.name);
5652
5779
  }
5653
5780
  this.#procedureSerializers = /* @__PURE__ */ Object.create(null);
5781
+ this.#procedureNameBytes = /* @__PURE__ */ Object.create(null);
5654
5782
  for (const procedure of remoteModule.procedures) {
5655
5783
  this.#procedureSerializers[procedure.name] = {
5656
5784
  serializeArgs: ProductType.makeSerializer(
@@ -5660,9 +5788,11 @@ var DbConnectionImpl = class {
5660
5788
  procedure.returnType.algebraicType
5661
5789
  )
5662
5790
  };
5791
+ this.#procedureNameBytes[procedure.name] = TEXT_ENCODER.encode(
5792
+ procedure.name
5793
+ );
5663
5794
  }
5664
- const connectionId = this.connectionId.toHexString();
5665
- url.searchParams.set("connection_id", connectionId);
5795
+ url.searchParams.set("connection_id", this.#connectionIdHex);
5666
5796
  this.clientCache = new ClientCache();
5667
5797
  this.db = this.#makeDbView();
5668
5798
  this.reducers = this.#makeReducers(remoteModule);
@@ -5670,7 +5800,7 @@ var DbConnectionImpl = class {
5670
5800
  this.wsPromise = createWSFn({
5671
5801
  url,
5672
5802
  nameOrAddress,
5673
- wsProtocol: "v2.bsatn.spacetimedb",
5803
+ wsProtocol: [...PREFERRED_WS_PROTOCOLS],
5674
5804
  authToken: token,
5675
5805
  compression,
5676
5806
  lightMode,
@@ -5714,16 +5844,22 @@ var DbConnectionImpl = class {
5714
5844
  }
5715
5845
  #makeReducers(def) {
5716
5846
  const out = {};
5717
- const writer = new BinaryWriter(1024);
5718
5847
  for (const reducer of def.reducers) {
5719
5848
  const reducerName = reducer.name;
5849
+ const encodedReducerName = this.#reducerNameBytes[reducerName];
5720
5850
  const key = reducer.accessorName;
5721
5851
  const { serialize: serializeArgs } = this.#reducerArgsSerializers[reducerName];
5722
5852
  out[key] = (params) => {
5853
+ const writer = this.#reducerArgsEncoder;
5723
5854
  writer.clear();
5724
5855
  serializeArgs(writer, params);
5725
5856
  const argsBuffer = writer.getBuffer();
5726
- return this.callReducer(reducerName, argsBuffer, params);
5857
+ return this.#callReducerWithEncodedName(
5858
+ reducerName,
5859
+ encodedReducerName,
5860
+ argsBuffer,
5861
+ params
5862
+ );
5727
5863
  };
5728
5864
  }
5729
5865
  return out;
@@ -5733,13 +5869,18 @@ var DbConnectionImpl = class {
5733
5869
  const writer = new BinaryWriter(1024);
5734
5870
  for (const procedure of def.procedures) {
5735
5871
  const procedureName = procedure.name;
5872
+ const encodedProcedureName = this.#procedureNameBytes[procedureName];
5736
5873
  const key = procedure.accessorName;
5737
5874
  const { serializeArgs, deserializeReturn } = this.#procedureSerializers[procedureName];
5738
5875
  out[key] = (params) => {
5739
5876
  writer.clear();
5740
5877
  serializeArgs(writer, params);
5741
5878
  const argsBuffer = writer.getBuffer();
5742
- return this.callProcedure(procedureName, argsBuffer).then((returnBuf) => {
5879
+ return this.#callProcedureWithEncodedName(
5880
+ procedureName,
5881
+ encodedProcedureName,
5882
+ argsBuffer
5883
+ ).then((returnBuf) => {
5743
5884
  return deserializeReturn(new BinaryReader(returnBuf));
5744
5885
  });
5745
5886
  };
@@ -5751,8 +5892,8 @@ var DbConnectionImpl = class {
5751
5892
  db: this.db,
5752
5893
  reducers: this.reducers,
5753
5894
  isActive: this.isActive,
5754
- subscriptionBuilder: this.subscriptionBuilder.bind(this),
5755
- disconnect: this.disconnect.bind(this),
5895
+ subscriptionBuilder: this.#boundSubscriptionBuilder,
5896
+ disconnect: this.#boundDisconnect,
5756
5897
  event
5757
5898
  };
5758
5899
  }
@@ -5797,21 +5938,16 @@ var DbConnectionImpl = class {
5797
5938
  }
5798
5939
  #parseRowList(type, tableName, rowList) {
5799
5940
  const buffer = rowList.rowsData;
5800
- const reader = new BinaryReader(buffer);
5941
+ const reader = this.#rowListReader;
5942
+ reader.reset(buffer);
5801
5943
  const rows = [];
5802
5944
  const deserializeRow = this.#rowDeserializers[tableName];
5803
- const table2 = this.#sourceNameToTableDef[tableName];
5804
- const columnsArray = Object.entries(table2.columns);
5805
- const primaryKeyColumnEntry = columnsArray.find(
5806
- (col) => col[1].columnMetadata.isPrimaryKey
5807
- );
5945
+ const { primaryKeyColName, primaryKeyColType } = this.#rowIdMetadata[tableName];
5808
5946
  let previousOffset = 0;
5809
5947
  while (reader.remaining > 0) {
5810
5948
  const row = deserializeRow(reader);
5811
5949
  let rowId = void 0;
5812
- if (primaryKeyColumnEntry !== void 0) {
5813
- const primaryKeyColName = primaryKeyColumnEntry[0];
5814
- const primaryKeyColType = primaryKeyColumnEntry[1].typeBuilder.algebraicType;
5950
+ if (primaryKeyColName !== void 0 && primaryKeyColType !== void 0) {
5815
5951
  rowId = AlgebraicType.intoMapKey(
5816
5952
  primaryKeyColType,
5817
5953
  row[primaryKeyColName]
@@ -5892,40 +6028,136 @@ var DbConnectionImpl = class {
5892
6028
  return this.#mergeTableUpdates(updates);
5893
6029
  }
5894
6030
  #flushOutboundQueue(wsResolved) {
6031
+ if (this.#negotiatedWsProtocol === V3_WS_PROTOCOL) {
6032
+ this.#flushOutboundQueueV3(wsResolved);
6033
+ return;
6034
+ }
6035
+ this.#flushOutboundQueueV2(wsResolved);
6036
+ }
6037
+ #flushOutboundQueueV2(wsResolved) {
5895
6038
  const pending = this.#outboundQueue.splice(0);
5896
6039
  for (const message of pending) {
5897
6040
  wsResolved.send(message);
5898
6041
  }
5899
6042
  }
6043
+ #flushOutboundQueueV3(wsResolved) {
6044
+ if (this.#outboundQueue.length === 0) {
6045
+ return;
6046
+ }
6047
+ const batchSize = countClientMessagesForV3Frame(
6048
+ this.#outboundQueue,
6049
+ MAX_V3_OUTBOUND_FRAME_BYTES
6050
+ );
6051
+ wsResolved.send(
6052
+ encodeClientMessagesV3(
6053
+ this.#clientFrameEncoder,
6054
+ this.#outboundQueue,
6055
+ batchSize
6056
+ )
6057
+ );
6058
+ if (batchSize === this.#outboundQueue.length) {
6059
+ this.#outboundQueue.length = 0;
6060
+ return;
6061
+ }
6062
+ this.#outboundQueue.copyWithin(0, batchSize);
6063
+ this.#outboundQueue.length -= batchSize;
6064
+ if (this.#outboundQueue.length > 0) {
6065
+ this.#scheduleDeferredOutboundFlush();
6066
+ }
6067
+ }
6068
+ #scheduleOutboundFlush() {
6069
+ this.#scheduleOutboundFlushWith("microtask");
6070
+ }
6071
+ #scheduleDeferredOutboundFlush() {
6072
+ this.#scheduleOutboundFlushWith("next-task");
6073
+ }
6074
+ #scheduleOutboundFlushWith(schedule) {
6075
+ if (this.#isOutboundFlushScheduled) {
6076
+ return;
6077
+ }
6078
+ this.#isOutboundFlushScheduled = true;
6079
+ const flush = () => {
6080
+ this.#isOutboundFlushScheduled = false;
6081
+ if (this.ws && this.isActive) {
6082
+ this.#flushOutboundQueue(this.ws);
6083
+ }
6084
+ };
6085
+ if (schedule === "next-task") {
6086
+ setTimeout(flush, 0);
6087
+ } else {
6088
+ queueMicrotask(flush);
6089
+ }
6090
+ }
6091
+ #reducerArgsEncoder = new BinaryWriter(1024);
5900
6092
  #clientMessageEncoder = new BinaryWriter(1024);
6093
+ #sendEncodedMessage(encoded, describe) {
6094
+ stdbLogger("trace", describe);
6095
+ if (this.ws && this.isActive) {
6096
+ if (this.#negotiatedWsProtocol === V2_WS_PROTOCOL) {
6097
+ if (this.#outboundQueue.length) this.#flushOutboundQueue(this.ws);
6098
+ this.ws.send(encoded);
6099
+ return;
6100
+ }
6101
+ this.#outboundQueue.push(encoded.slice());
6102
+ this.#scheduleOutboundFlush();
6103
+ } else {
6104
+ this.#outboundQueue.push(encoded.slice());
6105
+ }
6106
+ }
5901
6107
  #sendMessage(message) {
5902
6108
  const writer = this.#clientMessageEncoder;
5903
6109
  writer.clear();
5904
6110
  ClientMessage.serialize(writer, message);
5905
6111
  const encoded = writer.getBuffer();
5906
- if (this.ws && this.isActive) {
5907
- if (this.#outboundQueue.length) this.#flushOutboundQueue(this.ws);
5908
- stdbLogger(
5909
- "trace",
5910
- () => `Sending message to server: ${stringify(message)}`
5911
- );
5912
- this.ws.send(encoded);
5913
- } else {
5914
- stdbLogger(
5915
- "trace",
5916
- () => `Queuing message to server: ${stringify(message)}`
5917
- );
5918
- this.#outboundQueue.push(encoded.slice());
5919
- }
6112
+ const isLive = !!(this.ws && this.isActive);
6113
+ this.#sendEncodedMessage(
6114
+ encoded,
6115
+ () => isLive ? `Sending message to server: ${stringify(message)}` : `Queuing message to server: ${stringify(message)}`
6116
+ );
6117
+ }
6118
+ #sendCallReducerMessage(requestId, reducerNameBytes, argsBuffer) {
6119
+ const writer = this.#clientMessageEncoder;
6120
+ writer.clear();
6121
+ writer.writeByte(CLIENT_MESSAGE_CALL_REDUCER_TAG);
6122
+ writer.writeU32(requestId);
6123
+ writer.writeU8(0);
6124
+ writer.writeUInt8Array(reducerNameBytes);
6125
+ writer.writeUInt8Array(argsBuffer);
6126
+ const encoded = writer.getBuffer();
6127
+ this.#sendEncodedMessage(
6128
+ encoded,
6129
+ () => `Sending reducer call message to server: requestId=${requestId}`
6130
+ );
6131
+ }
6132
+ #sendCallProcedureMessage(requestId, procedureNameBytes, argsBuffer) {
6133
+ const writer = this.#clientMessageEncoder;
6134
+ writer.clear();
6135
+ writer.writeByte(CLIENT_MESSAGE_CALL_PROCEDURE_TAG);
6136
+ writer.writeU32(requestId);
6137
+ writer.writeU8(0);
6138
+ writer.writeUInt8Array(procedureNameBytes);
6139
+ writer.writeUInt8Array(argsBuffer);
6140
+ const encoded = writer.getBuffer();
6141
+ this.#sendEncodedMessage(
6142
+ encoded,
6143
+ () => `Sending procedure call message to server: requestId=${requestId}`
6144
+ );
6145
+ }
6146
+ #setConnectionId(connectionId) {
6147
+ this.connectionId = connectionId;
6148
+ this.#connectionIdHex = connectionId.toHexString();
5920
6149
  }
5921
6150
  #nextEventId() {
5922
6151
  this.#eventId += 1;
5923
- return `${this.connectionId.toHexString()}:${this.#eventId}`;
6152
+ return `${this.#connectionIdHex}:${this.#eventId}`;
5924
6153
  }
5925
6154
  /**
5926
6155
  * Handles WebSocket onOpen event.
5927
6156
  */
5928
6157
  #handleOnOpen() {
6158
+ if (this.ws) {
6159
+ this.#negotiatedWsProtocol = normalizeWsProtocol(this.ws.protocol);
6160
+ }
5929
6161
  this.isActive = true;
5930
6162
  if (this.ws) {
5931
6163
  this.#flushOutboundQueue(this.ws);
@@ -5960,8 +6192,16 @@ var DbConnectionImpl = class {
5960
6192
  eventContext
5961
6193
  );
5962
6194
  }
5963
- async #processMessage(data) {
5964
- const serverMessage = ServerMessage.deserialize(new BinaryReader(data));
6195
+ #dispatchPendingCallbacks(callbacks) {
6196
+ stdbLogger(
6197
+ "trace",
6198
+ () => `Calling ${callbacks.length} triggered row callbacks`
6199
+ );
6200
+ for (const callback of callbacks) {
6201
+ callback.cb();
6202
+ }
6203
+ }
6204
+ #processServerMessage(serverMessage) {
5965
6205
  stdbLogger(
5966
6206
  "trace",
5967
6207
  () => `Processing server message: ${stringify(serverMessage)}`
@@ -5972,7 +6212,7 @@ var DbConnectionImpl = class {
5972
6212
  if (!this.token && serverMessage.value.token) {
5973
6213
  this.token = serverMessage.value.token;
5974
6214
  }
5975
- this.connectionId = serverMessage.value.connectionId;
6215
+ this.#setConnectionId(serverMessage.value.connectionId);
5976
6216
  this.#emitter.emit("connect", this, this.identity, this.token);
5977
6217
  break;
5978
6218
  }
@@ -5998,13 +6238,7 @@ var DbConnectionImpl = class {
5998
6238
  const callbacks = this.#applyTableUpdates(tableUpdates, eventContext);
5999
6239
  const { event: _, ...subscriptionEventContext } = eventContext;
6000
6240
  subscription.emitter.emit("applied", subscriptionEventContext);
6001
- stdbLogger(
6002
- "trace",
6003
- () => `Calling ${callbacks.length} triggered row callbacks`
6004
- );
6005
- for (const callback of callbacks) {
6006
- callback.cb();
6007
- }
6241
+ this.#dispatchPendingCallbacks(callbacks);
6008
6242
  break;
6009
6243
  }
6010
6244
  case "UnsubscribeApplied": {
@@ -6027,13 +6261,7 @@ var DbConnectionImpl = class {
6027
6261
  const { event: _, ...subscriptionEventContext } = eventContext;
6028
6262
  subscription.emitter.emit("end", subscriptionEventContext);
6029
6263
  this.#subscriptionManager.subscriptions.delete(querySetId);
6030
- stdbLogger(
6031
- "trace",
6032
- () => `Calling ${callbacks.length} triggered row callbacks`
6033
- );
6034
- for (const callback of callbacks) {
6035
- callback.cb();
6036
- }
6264
+ this.#dispatchPendingCallbacks(callbacks);
6037
6265
  break;
6038
6266
  }
6039
6267
  case "SubscriptionError": {
@@ -6081,13 +6309,7 @@ var DbConnectionImpl = class {
6081
6309
  eventContext,
6082
6310
  serverMessage.value
6083
6311
  );
6084
- stdbLogger(
6085
- "trace",
6086
- () => `Calling ${callbacks.length} triggered row callbacks`
6087
- );
6088
- for (const callback of callbacks) {
6089
- callback.cb();
6090
- }
6312
+ this.#dispatchPendingCallbacks(callbacks);
6091
6313
  break;
6092
6314
  }
6093
6315
  case "ReducerResult": {
@@ -6115,13 +6337,7 @@ var DbConnectionImpl = class {
6115
6337
  eventContext,
6116
6338
  result.value.transactionUpdate
6117
6339
  );
6118
- stdbLogger(
6119
- "trace",
6120
- () => `Calling ${callbacks.length} triggered row callbacks`
6121
- );
6122
- for (const callback of callbacks) {
6123
- callback.cb();
6124
- }
6340
+ this.#dispatchPendingCallbacks(callbacks);
6125
6341
  }
6126
6342
  this.#reducerCallInfo.delete(requestId);
6127
6343
  const cb = this.#reducerCallbacks.get(requestId);
@@ -6146,14 +6362,55 @@ var DbConnectionImpl = class {
6146
6362
  }
6147
6363
  }
6148
6364
  }
6365
+ #processV2Message(data) {
6366
+ const reader = this.#messageReader;
6367
+ reader.reset(data);
6368
+ this.#processServerMessage(ServerMessage.deserialize(reader));
6369
+ }
6370
+ #processMessage(data) {
6371
+ if (this.#negotiatedWsProtocol !== V3_WS_PROTOCOL) {
6372
+ this.#processV2Message(data);
6373
+ return;
6374
+ }
6375
+ const messageCount = forEachServerMessageV3(
6376
+ this.#messageReader,
6377
+ data,
6378
+ (serverMessage) => {
6379
+ this.#processServerMessage(serverMessage);
6380
+ }
6381
+ );
6382
+ stdbLogger(
6383
+ "trace",
6384
+ () => `Processing server v3 payload with ${messageCount} message(s)`
6385
+ );
6386
+ }
6149
6387
  /**
6150
6388
  * Handles WebSocket onMessage event.
6151
6389
  * @param wsMessage MessageEvent object.
6152
6390
  */
6153
6391
  #handleOnMessage(wsMessage) {
6154
- this.#messageQueue = this.#messageQueue.then(() => {
6155
- return this.#processMessage(wsMessage.data);
6156
- });
6392
+ this.#inboundQueue.push(wsMessage.data);
6393
+ if (this.#isDrainingInboundQueue) {
6394
+ return;
6395
+ }
6396
+ this.#isDrainingInboundQueue = true;
6397
+ try {
6398
+ while (this.#inboundQueueOffset < this.#inboundQueue.length) {
6399
+ const data = this.#inboundQueue[this.#inboundQueueOffset];
6400
+ this.#inboundQueueOffset += 1;
6401
+ if (data) {
6402
+ this.#processMessage(data);
6403
+ }
6404
+ }
6405
+ } finally {
6406
+ if (this.#inboundQueueOffset >= this.#inboundQueue.length) {
6407
+ this.#inboundQueue.length = 0;
6408
+ } else if (this.#inboundQueueOffset > 0) {
6409
+ this.#inboundQueue = this.#inboundQueue.slice(this.#inboundQueueOffset);
6410
+ }
6411
+ this.#inboundQueueOffset = 0;
6412
+ this.#isDrainingInboundQueue = false;
6413
+ }
6157
6414
  }
6158
6415
  /**
6159
6416
  * Call a reducer on your SpacetimeDB module.
@@ -6162,6 +6419,45 @@ var DbConnectionImpl = class {
6162
6419
  * @param argsSerializer The arguments to pass to the reducer
6163
6420
  */
6164
6421
  callReducer(reducerName, argsBuffer, reducerArgs) {
6422
+ const encodedReducerName = this.#reducerNameBytes[reducerName];
6423
+ if (encodedReducerName) {
6424
+ return this.#callReducerWithEncodedName(
6425
+ reducerName,
6426
+ encodedReducerName,
6427
+ argsBuffer,
6428
+ reducerArgs
6429
+ );
6430
+ }
6431
+ return this.#callReducerGeneric(reducerName, argsBuffer, reducerArgs);
6432
+ }
6433
+ #callReducerWithEncodedName(reducerName, encodedReducerName, argsBuffer, reducerArgs) {
6434
+ const { promise, resolve, reject } = Promise.withResolvers();
6435
+ const requestId = this.#getNextRequestId();
6436
+ this.#sendCallReducerMessage(requestId, encodedReducerName, argsBuffer);
6437
+ if (reducerArgs) {
6438
+ this.#reducerCallInfo.set(requestId, {
6439
+ name: reducerName,
6440
+ args: reducerArgs
6441
+ });
6442
+ }
6443
+ this.#reducerCallbacks.set(requestId, (result) => {
6444
+ if (result.tag === "Ok" || result.tag === "OkEmpty") {
6445
+ resolve();
6446
+ } else {
6447
+ if (result.tag === "Err") {
6448
+ const reader = new BinaryReader(result.value);
6449
+ const errorString = reader.readString();
6450
+ reject(new SenderError(errorString));
6451
+ } else if (result.tag === "InternalError") {
6452
+ reject(new InternalError(result.value));
6453
+ } else {
6454
+ reject(new Error("Unexpected reducer result"));
6455
+ }
6456
+ }
6457
+ });
6458
+ return promise;
6459
+ }
6460
+ #callReducerGeneric(reducerName, argsBuffer, reducerArgs) {
6165
6461
  const { promise, resolve, reject } = Promise.withResolvers();
6166
6462
  const requestId = this.#getNextRequestId();
6167
6463
  const message = ClientMessage.CallReducer({
@@ -6201,7 +6497,8 @@ var DbConnectionImpl = class {
6201
6497
  * @param params The arguments to pass to the reducer
6202
6498
  */
6203
6499
  callReducerWithParams(reducerName, _paramsType, params) {
6204
- const writer = new BinaryWriter(1024);
6500
+ const writer = this.#reducerArgsEncoder;
6501
+ writer.clear();
6205
6502
  this.#reducerArgsSerializers[reducerName].serialize(writer, params);
6206
6503
  const argsBuffer = writer.getBuffer();
6207
6504
  return this.callReducer(reducerName, argsBuffer, params);
@@ -6213,6 +6510,30 @@ var DbConnectionImpl = class {
6213
6510
  * @param argsBuffer The arguments to pass to the reducer
6214
6511
  */
6215
6512
  callProcedure(procedureName, argsBuffer) {
6513
+ const encodedProcedureName = this.#procedureNameBytes[procedureName];
6514
+ if (encodedProcedureName) {
6515
+ return this.#callProcedureWithEncodedName(
6516
+ procedureName,
6517
+ encodedProcedureName,
6518
+ argsBuffer
6519
+ );
6520
+ }
6521
+ return this.#callProcedureGeneric(procedureName, argsBuffer);
6522
+ }
6523
+ #callProcedureWithEncodedName(procedureName, encodedProcedureName, argsBuffer) {
6524
+ const { promise, resolve, reject } = Promise.withResolvers();
6525
+ const requestId = this.#getNextRequestId();
6526
+ this.#sendCallProcedureMessage(requestId, encodedProcedureName, argsBuffer);
6527
+ this.#procedureCallbacks.set(requestId, (result) => {
6528
+ if (result.tag === "Ok") {
6529
+ resolve(result.value);
6530
+ } else {
6531
+ reject(result.value);
6532
+ }
6533
+ });
6534
+ return promise;
6535
+ }
6536
+ #callProcedureGeneric(procedureName, argsBuffer) {
6216
6537
  const { promise, resolve, reject } = Promise.withResolvers();
6217
6538
  const requestId = this.#getNextRequestId();
6218
6539
  const message = ClientMessage.CallProcedure({
@@ -7153,7 +7474,7 @@ function table(opts, row, ..._) {
7153
7474
  increment: 1n
7154
7475
  });
7155
7476
  }
7156
- if (meta.defaultValue) {
7477
+ if (Object.prototype.hasOwnProperty.call(meta, "defaultValue")) {
7157
7478
  const writer = new BinaryWriter(16);
7158
7479
  builder.serialize(writer, meta.defaultValue);
7159
7480
  defaultValues.push({