@zeronsh/orbit 0.3.4 → 0.3.6

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.
@@ -407,7 +407,7 @@ var Store = class {
407
407
  // persistence
408
408
  #kv;
409
409
  #dirtyRows = /* @__PURE__ */ new Set();
410
- // "tablepkKey"
410
+ // `${table}\u0000${pkKey}` (NUL separator: cannot appear in a table name)
411
411
  #dirtyPending = /* @__PURE__ */ new Set();
412
412
  #cleared = false;
413
413
  #flushTimer;
@@ -606,29 +606,36 @@ var Store = class {
606
606
  }
607
607
  const kv = this.#kv;
608
608
  if (!kv) return;
609
+ const clearOps = [];
609
610
  if (this.#cleared) {
610
- const dels = [];
611
- for (const [k] of await kv.entries("e/")) dels.push(kv.del(k));
612
- await Promise.all(dels);
611
+ for (const [k] of await kv.entries("e/")) clearOps.push({ type: "del", key: k });
613
612
  this.#cleared = false;
614
613
  }
615
- const ps = [];
614
+ const ops = [];
616
615
  for (const dk of this.#dirtyRows) {
617
616
  const sep = dk.indexOf("\0");
618
617
  const table2 = dk.slice(0, sep);
619
618
  const pkKey2 = dk.slice(sep + 1);
620
619
  const row = this.#tables.get(table2)?.get(pkKey2);
621
620
  const key = `e/${table2}/${pkKey2}`;
622
- ps.push(row ? kv.set(key, { t: table2, k: pkKey2, v: row }) : kv.del(key));
621
+ ops.push(row ? { type: "set", key, value: { t: table2, k: pkKey2, v: row } } : { type: "del", key });
623
622
  }
624
623
  this.#dirtyRows.clear();
625
624
  for (const id of this.#dirtyPending) {
626
625
  const p = this.#pending.find((x) => x.id === id);
627
- ps.push(p ? kv.set(`p/${id}`, p) : kv.del(`p/${id}`));
626
+ ops.push(p ? { type: "set", key: `p/${id}`, value: p } : { type: "del", key: `p/${id}` });
628
627
  }
629
628
  this.#dirtyPending.clear();
630
- await Promise.all(ps);
631
- if (this.#cookieDirty && this.#cookie !== void 0) {
629
+ const cookieDirty = this.#cookieDirty && this.#cookie !== void 0;
630
+ if (kv.batch) {
631
+ if (cookieDirty) ops.push({ type: "set", key: "cookie", value: this.#cookie });
632
+ await kv.batch([...clearOps, ...ops]);
633
+ if (cookieDirty) this.#cookieDirty = false;
634
+ return;
635
+ }
636
+ await Promise.all(clearOps.map((o) => kv.del(o.key)));
637
+ await Promise.all(ops.map((o) => o.type === "set" ? kv.set(o.key, o.value) : kv.del(o.key)));
638
+ if (cookieDirty) {
632
639
  await kv.set("cookie", this.#cookie);
633
640
  this.#cookieDirty = false;
634
641
  }
@@ -1709,6 +1716,8 @@ var Orbit = class {
1709
1716
  #onError;
1710
1717
  /** Whether the id was supplied explicitly (then we never override it from the KV). */
1711
1718
  #idFromOpts;
1719
+ /** Releases the Web-Locks persistence leadership (held by the first tab). */
1720
+ #releasePersistLock;
1712
1721
  constructor(opts) {
1713
1722
  this.#opts = opts;
1714
1723
  this.#schema = opts.schema;
@@ -1827,6 +1836,7 @@ var Orbit = class {
1827
1836
  close() {
1828
1837
  this.#closed = true;
1829
1838
  this.#ws?.close();
1839
+ this.#releasePersistLock?.();
1830
1840
  }
1831
1841
  // --- internals ----------------------------------------------------------
1832
1842
  /** Resolve the client context (a value or sync getter); `{}` when unset. */
@@ -1834,8 +1844,39 @@ var Orbit = class {
1834
1844
  const c = this.#context;
1835
1845
  return typeof c === "function" ? c() : c ?? {};
1836
1846
  }
1847
+ /**
1848
+ * Web-Locks single-writer election for the persisted cache. Two tabs sharing one
1849
+ * IndexedDB (and the restored clientID) would each flush their own dirty rows,
1850
+ * pending mutations, and cookie into the same flat keyspace with no coordination —
1851
+ * last-writer-wins can persist a cookie covering rows only the *other* tab wrote,
1852
+ * and both tabs would drive the same server-side CVR. Only the first tab (the
1853
+ * lock holder) gets persistence; later tabs run memory-only with their own fresh
1854
+ * clientID (a full server sync — correct, just uncached). The lock auto-releases
1855
+ * when the tab dies, so the next reload elects a new leader. Environments without
1856
+ * Web Locks (Node, tests, old browsers) keep today's behavior.
1857
+ */
1858
+ async #acquirePersistLock() {
1859
+ const locks = globalThis.navigator?.locks;
1860
+ if (!locks) return true;
1861
+ return new Promise((resolve2) => {
1862
+ const req = locks.request("zeronsh-orbit-persist", { ifAvailable: true }, (lock) => {
1863
+ if (lock === null) {
1864
+ resolve2(false);
1865
+ return;
1866
+ }
1867
+ resolve2(true);
1868
+ return new Promise((release) => {
1869
+ this.#releasePersistLock = release;
1870
+ });
1871
+ });
1872
+ void Promise.resolve(req).catch(() => resolve2(true));
1873
+ });
1874
+ }
1837
1875
  /** Hydrate persisted state (if any), restore unconfirmed mutations, then connect. */
1838
1876
  async #init() {
1877
+ if (this.#kv && !await this.#acquirePersistLock()) {
1878
+ this.#kv = void 0;
1879
+ }
1839
1880
  if (this.#kv) {
1840
1881
  try {
1841
1882
  await this.#store.hydrate(this.#kv);
@@ -1881,8 +1922,13 @@ var Orbit = class {
1881
1922
  requestID: Math.random().toString(36).slice(2)
1882
1923
  }];
1883
1924
  this.#unconfirmedPushes.set(id, msg);
1884
- void this.#kv?.set("nextMutationID", this.#nextMutationID);
1885
- this.#send(msg);
1925
+ const kv = this.#kv;
1926
+ if (kv) {
1927
+ void kv.set("nextMutationID", this.#nextMutationID).catch(() => {
1928
+ }).then(() => this.#send(msg));
1929
+ } else {
1930
+ this.#send(msg);
1931
+ }
1886
1932
  }
1887
1933
  async #connect() {
1888
1934
  if (this.#closed || this.#connecting) return;
@@ -2004,6 +2050,12 @@ var MemoryKV = class {
2004
2050
  async entries(prefix) {
2005
2051
  return [...this.#m].filter(([k]) => k.startsWith(prefix));
2006
2052
  }
2053
+ async batch(ops) {
2054
+ for (const op of ops) {
2055
+ if (op.type === "set") this.#m.set(op.key, op.value);
2056
+ else this.#m.delete(op.key);
2057
+ }
2058
+ }
2007
2059
  };
2008
2060
  function wrap(req) {
2009
2061
  return new Promise((resolve2, reject) => {
@@ -2036,16 +2088,33 @@ var IDBKV = class {
2036
2088
  }
2037
2089
  async entries(prefix) {
2038
2090
  const s = await this.#store("readonly");
2039
- const [keys, vals] = await Promise.all([wrap(s.getAllKeys()), wrap(s.getAll())]);
2040
- const out = [];
2041
- for (let i = 0; i < keys.length; i++) {
2042
- const k = String(keys[i]);
2043
- if (k.startsWith(prefix)) out.push([k, vals[i]]);
2044
- }
2045
- return out;
2091
+ const successor = prefix.slice(0, -1) + String.fromCharCode(prefix.charCodeAt(prefix.length - 1) + 1);
2092
+ const range = IDBKeyRange.bound(prefix, successor, false, true);
2093
+ const [keys, vals] = await Promise.all([wrap(s.getAllKeys(range)), wrap(s.getAll(range))]);
2094
+ return keys.map((k, i) => [String(k), vals[i]]);
2095
+ }
2096
+ /**
2097
+ * All ops in ONE readwrite transaction: atomic under a crash (IndexedDB
2098
+ * transactions are all-or-nothing) and one commit instead of one per key —
2099
+ * the flush of a large poke goes from N transactions to 1.
2100
+ */
2101
+ async batch(ops) {
2102
+ if (ops.length === 0) return;
2103
+ const db = await this.#dbp;
2104
+ await new Promise((resolve2, reject) => {
2105
+ const tx = db.transaction("kv", "readwrite");
2106
+ const s = tx.objectStore("kv");
2107
+ for (const op of ops) {
2108
+ if (op.type === "set") s.put(op.value, op.key);
2109
+ else s.delete(op.key);
2110
+ }
2111
+ tx.oncomplete = () => resolve2();
2112
+ tx.onerror = () => reject(tx.error);
2113
+ tx.onabort = () => reject(tx.error);
2114
+ });
2046
2115
  }
2047
2116
  };
2048
2117
 
2049
2118
  export { IDBKV, MaterializedView, MemoryKV, MemorySource, MemorySourceProvider, Orbit, PROTOCOL_VERSION, Query, QueryManager, SchemaQuery, SourceConnection, Store, StoreProvider, TypedQuery, View, boolean, buildPipeline, buildSchemaQueries, collectOps, compareValues, createBuilder, createOrbitApi, createSchema, defineMutation, defineQuery, evaluate, hashAST, hashString, json, nodeToRow, number, optional, parseTTL, relationships, string, table, tablesOf, unwrapSingular, validateArgs, valuesEqual };
2050
- //# sourceMappingURL=chunk-LUFJQUF4.js.map
2051
- //# sourceMappingURL=chunk-LUFJQUF4.js.map
2119
+ //# sourceMappingURL=chunk-B575ZNM2.js.map
2120
+ //# sourceMappingURL=chunk-B575ZNM2.js.map