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