@fairfox/polly 0.49.0 → 0.51.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.
package/dist/src/mesh.js CHANGED
@@ -1019,12 +1019,15 @@ var DEFAULT_MESH_KEY_ID = "polly-mesh-default";
1019
1019
 
1020
1020
  class MeshNetworkAdapter extends NetworkAdapter {
1021
1021
  base;
1022
- keyring;
1022
+ keyringSource;
1023
1023
  encryptionEnabled;
1024
+ get keyring() {
1025
+ return this.keyringSource();
1026
+ }
1024
1027
  constructor(options) {
1025
1028
  super();
1026
1029
  this.base = options.base;
1027
- this.keyring = options.keyring;
1030
+ this.keyringSource = options.keyringSource;
1028
1031
  this.encryptionEnabled = options.encryptionEnabled ?? true;
1029
1032
  this.base.on("close", () => this.emit("close"));
1030
1033
  this.base.on("peer-candidate", (payload) => this.emit("peer-candidate", payload));
@@ -1057,10 +1060,11 @@ class MeshNetworkAdapter extends NetworkAdapter {
1057
1060
  this.base.send(wrapped);
1058
1061
  }
1059
1062
  wrap(message) {
1063
+ const keyring = this.keyringSource();
1060
1064
  const serialised = serialiseMessage(message);
1061
1065
  let payloadToSign;
1062
1066
  if (this.encryptionEnabled) {
1063
- const docKey = this.keyring.documentKeys.get(DEFAULT_MESH_KEY_ID);
1067
+ const docKey = keyring.documentKeys.get(DEFAULT_MESH_KEY_ID);
1064
1068
  if (!docKey) {
1065
1069
  throw new Error(`MeshNetworkAdapter: missing document encryption key under id "${DEFAULT_MESH_KEY_ID}". Provision the key in the keyring before sending.`);
1066
1070
  }
@@ -1069,7 +1073,7 @@ class MeshNetworkAdapter extends NetworkAdapter {
1069
1073
  } else {
1070
1074
  payloadToSign = serialised;
1071
1075
  }
1072
- const signed = signEnvelope(payloadToSign, message.senderId, this.keyring.identity.secretKey);
1076
+ const signed = signEnvelope(payloadToSign, message.senderId, keyring.identity.secretKey);
1073
1077
  const signedBytes = encodeSignedEnvelope(signed);
1074
1078
  return {
1075
1079
  type: message.type,
@@ -1087,10 +1091,11 @@ class MeshNetworkAdapter extends NetworkAdapter {
1087
1091
  } catch {
1088
1092
  return;
1089
1093
  }
1090
- if (this.keyring.revokedPeers.has(signed.senderId)) {
1094
+ const keyring = this.keyringSource();
1095
+ if (keyring.revokedPeers.has(signed.senderId)) {
1091
1096
  return;
1092
1097
  }
1093
- const senderKey = this.keyring.knownPeers.get(signed.senderId);
1098
+ const senderKey = keyring.knownPeers.get(signed.senderId);
1094
1099
  if (!senderKey) {
1095
1100
  return;
1096
1101
  }
@@ -1109,7 +1114,7 @@ class MeshNetworkAdapter extends NetworkAdapter {
1109
1114
  } catch {
1110
1115
  return;
1111
1116
  }
1112
- const docKey = this.keyring.documentKeys.get(encrypted.documentId);
1117
+ const docKey = keyring.documentKeys.get(encrypted.documentId);
1113
1118
  if (!docKey) {
1114
1119
  return;
1115
1120
  }
@@ -1842,6 +1847,92 @@ function $meshList(key, initialValue, options = {}) {
1842
1847
  import {
1843
1848
  NetworkAdapter as NetworkAdapter2
1844
1849
  } from "@automerge/automerge-repo/slim";
1850
+
1851
+ // src/shared/lib/sync-fragment.ts
1852
+ var SYNC_FRAGMENT_THRESHOLD = 64 * 1024;
1853
+ var SYNC_FRAGMENT_CHUNK_SIZE = 64 * 1024;
1854
+ function serialiseSyncFragment(header, data) {
1855
+ const headerBytes = new TextEncoder().encode(JSON.stringify(header));
1856
+ const size = 4 + headerBytes.length + data.length;
1857
+ const buffer = new ArrayBuffer(size);
1858
+ const out = new Uint8Array(buffer);
1859
+ const view = new DataView(buffer);
1860
+ view.setUint32(0, headerBytes.length, false);
1861
+ out.set(headerBytes, 4);
1862
+ out.set(data, 4 + headerBytes.length);
1863
+ return out;
1864
+ }
1865
+ function parseSyncFragment(bytes) {
1866
+ if (bytes.length < 4)
1867
+ return;
1868
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
1869
+ const headerLen = view.getUint32(0, false);
1870
+ if (bytes.length < 4 + headerLen)
1871
+ return;
1872
+ try {
1873
+ const header = JSON.parse(new TextDecoder().decode(bytes.subarray(4, 4 + headerLen)));
1874
+ if (header.type !== "sync-fragment")
1875
+ return;
1876
+ const data = bytes.subarray(4 + headerLen);
1877
+ return { header, data };
1878
+ } catch {
1879
+ return;
1880
+ }
1881
+ }
1882
+ function isSyncFragmentType(bytes) {
1883
+ if (bytes.length < 4)
1884
+ return false;
1885
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
1886
+ const headerLen = view.getUint32(0, false);
1887
+ if (bytes.length < 4 + headerLen)
1888
+ return false;
1889
+ const headerSlice = bytes.subarray(4, 4 + headerLen);
1890
+ const needle = new TextEncoder().encode('"type":"sync-fragment"');
1891
+ return findSubarray2(headerSlice, needle) !== -1;
1892
+ }
1893
+ function chunkSyncMessage(bytes, id, chunkSize = SYNC_FRAGMENT_CHUNK_SIZE) {
1894
+ const total = Math.max(1, Math.ceil(bytes.length / chunkSize));
1895
+ const fragments = [];
1896
+ for (let index = 0;index < total; index++) {
1897
+ const start = index * chunkSize;
1898
+ const end = Math.min(start + chunkSize, bytes.length);
1899
+ fragments.push(serialiseSyncFragment({ type: "sync-fragment", id, index, total }, bytes.subarray(start, end)));
1900
+ }
1901
+ return fragments;
1902
+ }
1903
+ function reassembleSyncFragments(chunks, total) {
1904
+ let totalBytes = 0;
1905
+ for (let i = 0;i < total; i++) {
1906
+ const chunk = chunks.get(i);
1907
+ if (!chunk) {
1908
+ throw new Error(`reassembleSyncFragments: missing fragment ${i} of ${total}`);
1909
+ }
1910
+ totalBytes += chunk.length;
1911
+ }
1912
+ const out = new Uint8Array(totalBytes);
1913
+ let offset = 0;
1914
+ for (let i = 0;i < total; i++) {
1915
+ const chunk = chunks.get(i);
1916
+ out.set(chunk, offset);
1917
+ offset += chunk.length;
1918
+ }
1919
+ return out;
1920
+ }
1921
+ function findSubarray2(haystack, needle) {
1922
+ if (needle.length === 0)
1923
+ return 0;
1924
+ outer:
1925
+ for (let i = 0;i <= haystack.length - needle.length; i++) {
1926
+ for (let j = 0;j < needle.length; j++) {
1927
+ if (haystack[i + j] !== needle[j])
1928
+ continue outer;
1929
+ }
1930
+ return i;
1931
+ }
1932
+ return -1;
1933
+ }
1934
+
1935
+ // src/shared/lib/mesh-webrtc-adapter.ts
1845
1936
  var DEFAULT_ICE_SERVERS = [
1846
1937
  { urls: "stun:stun.l.google.com:19302" },
1847
1938
  { urls: "stun:stun1.l.google.com:19302" }
@@ -1960,11 +2051,22 @@ class MeshWebRTCAdapter extends NetworkAdapter2 {
1960
2051
  slot = this.createInitiatingSlot(targetId);
1961
2052
  }
1962
2053
  if (slot.channel && slot.channel.readyState === "open") {
1963
- slot.channel.send(bytes);
2054
+ this.sendBytesMaybeFragmented(slot.channel, bytes);
1964
2055
  } else {
1965
2056
  slot.pendingSends.push(bytes);
1966
2057
  }
1967
2058
  }
2059
+ sendBytesMaybeFragmented(channel, bytes) {
2060
+ if (bytes.length <= SYNC_FRAGMENT_THRESHOLD) {
2061
+ channel.send(bytes);
2062
+ return;
2063
+ }
2064
+ const id = crypto.randomUUID();
2065
+ const fragments = chunkSyncMessage(bytes, id);
2066
+ for (const fragment of fragments) {
2067
+ channel.send(fragment);
2068
+ }
2069
+ }
1968
2070
  handleSignal(fromPeerId, rawPayload) {
1969
2071
  const payload = rawPayload;
1970
2072
  if (!payload || typeof payload !== "object" || !("kind" in payload)) {
@@ -1985,7 +2087,12 @@ class MeshWebRTCAdapter extends NetworkAdapter2 {
1985
2087
  createInitiatingSlot(targetId) {
1986
2088
  const connection = new this.RTCPeerConnectionCtor({ iceServers: this.iceServers });
1987
2089
  const channel = connection.createDataChannel(this.dataChannelLabel, { ordered: true });
1988
- const slot = { connection, channel, pendingSends: [] };
2090
+ const slot = {
2091
+ connection,
2092
+ channel,
2093
+ pendingSends: [],
2094
+ pendingFragments: new Map
2095
+ };
1989
2096
  this.slots.set(targetId, slot);
1990
2097
  this.wireConnection(targetId, connection);
1991
2098
  this.wireDataChannel(targetId, channel);
@@ -2009,7 +2116,12 @@ class MeshWebRTCAdapter extends NetworkAdapter2 {
2009
2116
  this.slots.delete(fromPeerId);
2010
2117
  }
2011
2118
  const connection = new this.RTCPeerConnectionCtor({ iceServers: this.iceServers });
2012
- const slot = { connection, channel: undefined, pendingSends: [] };
2119
+ const slot = {
2120
+ connection,
2121
+ channel: undefined,
2122
+ pendingSends: [],
2123
+ pendingFragments: new Map
2124
+ };
2013
2125
  this.slots.set(fromPeerId, slot);
2014
2126
  this.wireConnection(fromPeerId, connection);
2015
2127
  connection.ondatachannel = (event) => {
@@ -2068,7 +2180,7 @@ class MeshWebRTCAdapter extends NetworkAdapter2 {
2068
2180
  if (!slot)
2069
2181
  return;
2070
2182
  for (const bytes of slot.pendingSends) {
2071
- channel.send(bytes);
2183
+ this.sendBytesMaybeFragmented(channel, bytes);
2072
2184
  }
2073
2185
  slot.pendingSends = [];
2074
2186
  };
@@ -2089,6 +2201,10 @@ class MeshWebRTCAdapter extends NetworkAdapter2 {
2089
2201
  }
2090
2202
  dispatchMessage(fromPeerId, bytes) {
2091
2203
  try {
2204
+ if (isSyncFragmentType(bytes)) {
2205
+ this.handleSyncFragment(fromPeerId, bytes);
2206
+ return;
2207
+ }
2092
2208
  if (this.onBlobMessage && isBlobMessageType(bytes)) {
2093
2209
  const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
2094
2210
  const headerLen = view.getUint32(0, false);
@@ -2101,6 +2217,26 @@ class MeshWebRTCAdapter extends NetworkAdapter2 {
2101
2217
  this.emit("message", message);
2102
2218
  } catch {}
2103
2219
  }
2220
+ handleSyncFragment(fromPeerId, bytes) {
2221
+ const parsed = parseSyncFragment(bytes);
2222
+ if (!parsed)
2223
+ return;
2224
+ const slot = this.slots.get(fromPeerId);
2225
+ if (!slot)
2226
+ return;
2227
+ const { header, data } = parsed;
2228
+ let entry = slot.pendingFragments.get(header.id);
2229
+ if (!entry) {
2230
+ entry = { chunks: new Map, total: header.total };
2231
+ slot.pendingFragments.set(header.id, entry);
2232
+ }
2233
+ entry.chunks.set(header.index, data.slice());
2234
+ if (entry.chunks.size < entry.total)
2235
+ return;
2236
+ slot.pendingFragments.delete(header.id);
2237
+ const reassembled = reassembleSyncFragments(entry.chunks, entry.total);
2238
+ this.dispatchMessage(fromPeerId, reassembled);
2239
+ }
2104
2240
  get connectedPeerIds() {
2105
2241
  const ids = [];
2106
2242
  for (const [peerId, slot] of this.slots) {
@@ -2165,7 +2301,8 @@ async function resolveIceServers(rtc) {
2165
2301
  return rtc?.iceServers;
2166
2302
  }
2167
2303
  async function createMeshClient(options) {
2168
- const keyring = await resolveKeyring(options.keyring);
2304
+ const keyringSource = await resolveKeyringSource(options.keyring);
2305
+ const keyring = keyringSource();
2169
2306
  const encryptionEnabled = options.encryptionEnabled ?? true;
2170
2307
  if (encryptionEnabled && !keyring.documentKeys.has(DEFAULT_MESH_KEY_ID)) {
2171
2308
  throw new Error(`createMeshClient: encryption is enabled but the keyring has no document key for "${DEFAULT_MESH_KEY_ID}". Bootstrap or apply a pairing token that carries the document key before connecting.`);
@@ -2210,7 +2347,7 @@ async function createMeshClient(options) {
2210
2347
  webrtcAdapter = new MeshWebRTCAdapter(webrtcAdapterOptions);
2211
2348
  const networkAdapter = new MeshNetworkAdapter({
2212
2349
  base: webrtcAdapter,
2213
- keyring,
2350
+ keyringSource,
2214
2351
  encryptionEnabled
2215
2352
  });
2216
2353
  const repo = new Repo({
@@ -2222,7 +2359,9 @@ async function createMeshClient(options) {
2222
2359
  await signaling.connect();
2223
2360
  return {
2224
2361
  repo,
2225
- keyring,
2362
+ get keyring() {
2363
+ return keyringSource();
2364
+ },
2226
2365
  signaling,
2227
2366
  networkAdapter,
2228
2367
  webrtcAdapter,
@@ -2233,15 +2372,18 @@ async function createMeshClient(options) {
2233
2372
  }
2234
2373
  };
2235
2374
  }
2236
- async function resolveKeyring(source) {
2375
+ async function resolveKeyringSource(source) {
2376
+ if (typeof source === "object" && source !== null && "source" in source) {
2377
+ return source.source;
2378
+ }
2237
2379
  if ("storage" in source) {
2238
2380
  const loaded = await source.storage.load();
2239
2381
  if (loaded === null) {
2240
2382
  throw new Error("createMeshClient: keyring storage returned null (no saved keyring). In a Node CLI, bootstrap with `bootstrapCliKeyring` from `@fairfox/polly/mesh/node`; in a browser, run your pairing flow first and save the keyring through the storage adapter before constructing the client.");
2241
2383
  }
2242
- return loaded;
2384
+ return () => loaded;
2243
2385
  }
2244
- return source;
2386
+ return () => source;
2245
2387
  }
2246
2388
  // src/shared/lib/pairing.ts
2247
2389
  init_encryption();
@@ -2637,4 +2779,4 @@ export {
2637
2779
  $meshCounter
2638
2780
  };
2639
2781
 
2640
- //# debugId=1DAAF5727BCAB57A64756E2164756E21
2782
+ //# debugId=FD1853161F9E945F64756E2164756E21