@dabble/patches 0.8.0 → 0.8.2

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 (51) hide show
  1. package/dist/algorithms/ot/shared/rebaseChanges.js +6 -5
  2. package/dist/client/LWWDoc.js +11 -24
  3. package/dist/client/Patches.js +2 -2
  4. package/dist/client/PatchesDoc.d.ts +1 -1
  5. package/dist/client/PatchesDoc.js +2 -3
  6. package/dist/client/index.d.ts +1 -1
  7. package/dist/{index-C7ZhU2kS.d.ts → index-BO6EQFpw.d.ts} +2 -14
  8. package/dist/index.d.ts +13 -13
  9. package/dist/index.js +2 -10
  10. package/dist/json-patch/index.d.ts +11 -11
  11. package/dist/json-patch/index.js +0 -1
  12. package/dist/json-patch/ops/index.d.ts +1 -1
  13. package/dist/json-patch/ops/index.js +15 -14
  14. package/dist/json-patch/utils/getType.d.ts +1 -1
  15. package/dist/micro/client.d.ts +44 -0
  16. package/dist/micro/client.js +224 -0
  17. package/dist/micro/doc.d.ts +51 -0
  18. package/dist/micro/doc.js +137 -0
  19. package/dist/micro/index.d.ts +7 -0
  20. package/dist/micro/index.js +33 -0
  21. package/dist/micro/ops.d.ts +22 -0
  22. package/dist/micro/ops.js +110 -0
  23. package/dist/micro/server.d.ts +47 -0
  24. package/dist/micro/server.js +263 -0
  25. package/dist/micro/types.d.ts +88 -0
  26. package/dist/micro/types.js +14 -0
  27. package/dist/net/PatchesClient.d.ts +2 -2
  28. package/dist/net/PatchesSync.d.ts +6 -0
  29. package/dist/net/PatchesSync.js +20 -8
  30. package/dist/net/http/FetchTransport.d.ts +1 -3
  31. package/dist/net/http/FetchTransport.js +5 -11
  32. package/dist/net/index.d.ts +2 -2
  33. package/dist/net/protocol/JSONRPCClient.js +7 -0
  34. package/dist/net/protocol/JSONRPCServer.d.ts +4 -6
  35. package/dist/net/protocol/JSONRPCServer.js +3 -5
  36. package/dist/net/protocol/types.d.ts +1 -10
  37. package/dist/net/rest/PatchesREST.d.ts +2 -2
  38. package/dist/server/LWWMemoryStoreBackend.d.ts +0 -1
  39. package/dist/server/LWWMemoryStoreBackend.js +0 -3
  40. package/dist/server/LWWServer.js +2 -1
  41. package/dist/server/OTBranchManager.d.ts +2 -7
  42. package/dist/server/OTBranchManager.js +0 -2
  43. package/dist/server/OTServer.js +2 -1
  44. package/dist/server/PatchesHistoryManager.d.ts +1 -10
  45. package/dist/server/PatchesHistoryManager.js +2 -18
  46. package/dist/server/index.d.ts +2 -2
  47. package/dist/server/index.js +3 -3
  48. package/dist/server/types.d.ts +0 -5
  49. package/dist/utils/concurrency.d.ts +6 -1
  50. package/dist/utils/concurrency.js +4 -0
  51. package/package.json +5 -1
@@ -20,12 +20,13 @@ function rebaseChanges(serverChanges, localChanges) {
20
20
  );
21
21
  const baseRev = lastChange.rev;
22
22
  let rev = lastChange.rev;
23
- const result = filteredLocalChanges.map((change) => {
24
- rev++;
23
+ const result = [];
24
+ for (const change of filteredLocalChanges) {
25
25
  const ops = transformPatch.transform(change.ops).ops;
26
- if (!ops.length) return null;
27
- return { ...change, baseRev, rev, ops };
28
- }).filter(Boolean);
26
+ if (!ops.length) continue;
27
+ rev++;
28
+ result.push({ ...change, baseRev, rev, ops });
29
+ }
29
30
  return result;
30
31
  }
31
32
  export {
@@ -17,13 +17,8 @@ class LWWDoc extends BaseDoc {
17
17
  this._committedRev = snapshot?.rev ?? 0;
18
18
  this._hasPending = (snapshot?.changes?.length ?? 0) > 0;
19
19
  if (snapshot?.changes && snapshot.changes.length > 0) {
20
- let currentState = this.state;
21
- for (const change of snapshot.changes) {
22
- for (const op of change.ops) {
23
- currentState = applyPatch(currentState, [op], { partial: true });
24
- }
25
- }
26
- this.state = currentState;
20
+ const allOps = snapshot.changes.flatMap((c) => c.ops);
21
+ this.state = applyPatch(this.state, allOps, { partial: true });
27
22
  }
28
23
  this._baseState = this.state;
29
24
  this._checkLoaded();
@@ -46,11 +41,11 @@ class LWWDoc extends BaseDoc {
46
41
  this._optimisticOps = [];
47
42
  let currentState = snapshot.state;
48
43
  if (snapshot.changes && snapshot.changes.length > 0) {
49
- for (const change of snapshot.changes) {
50
- for (const op of change.ops) {
51
- currentState = applyPatch(currentState, [op], { partial: true });
52
- }
53
- }
44
+ currentState = applyPatch(
45
+ currentState,
46
+ snapshot.changes.flatMap((c) => c.ops),
47
+ { partial: true }
48
+ );
54
49
  }
55
50
  this._baseState = currentState;
56
51
  this._checkLoaded();
@@ -97,20 +92,12 @@ class LWWDoc extends BaseDoc {
97
92
  this._hasPending = hasPending ?? hasPendingChanges;
98
93
  this._checkLoaded();
99
94
  if (hasServerChanges) {
100
- let newBaseState = this._baseState;
101
- for (const change of changes) {
102
- for (const op of change.ops) {
103
- newBaseState = applyPatch(newBaseState, [op], { partial: true });
104
- }
105
- }
106
- this._baseState = newBaseState;
95
+ const allOps = changes.flatMap((c) => c.ops);
96
+ this._baseState = applyPatch(this._baseState, allOps, { partial: true });
107
97
  this._recomputeState();
108
98
  } else {
109
- for (const change of changes) {
110
- for (const op of change.ops) {
111
- this._baseState = applyPatch(this._baseState, [op], { partial: true });
112
- }
113
- }
99
+ const allOps = changes.flatMap((c) => c.ops);
100
+ this._baseState = applyPatch(this._baseState, allOps, { partial: true });
114
101
  if (this._optimisticOps.length > 0) {
115
102
  this._optimisticOps.shift();
116
103
  } else {
@@ -102,8 +102,6 @@ class Patches {
102
102
  if (!docIds.length) return;
103
103
  docIds.forEach(this.trackedDocs.delete, this.trackedDocs);
104
104
  this.onUntrackDocs.emit(docIds);
105
- const closedPromises = docIds.filter((id) => this.docs.has(id)).map((id) => this.closeDoc(id));
106
- await Promise.all(closedPromises);
107
105
  const byAlgorithm = /* @__PURE__ */ new Map();
108
106
  for (const docId of docIds) {
109
107
  const managed = this.docs.get(docId);
@@ -112,6 +110,8 @@ class Patches {
112
110
  list.push(docId);
113
111
  byAlgorithm.set(algorithm, list);
114
112
  }
113
+ const closedPromises = docIds.filter((id) => this.docs.has(id)).map((id) => this.closeDoc(id));
114
+ await Promise.all(closedPromises);
115
115
  await Promise.all([...byAlgorithm.entries()].map(([algorithm, ids]) => algorithm.untrackDocs(ids)));
116
116
  }
117
117
  // ensure a second call to openDoc with the same docId returns the same promise while opening
@@ -1,6 +1,6 @@
1
1
  import 'easy-signal';
2
2
  import '../json-patch/types.js';
3
3
  import '../types.js';
4
- export { O as OTDoc, P as PatchesDoc, O as PatchesDocClass, a as PatchesDocOptions } from '../BaseDoc-BT18xPxU.js';
4
+ export { O as OTDoc, P as PatchesDoc, a as PatchesDocOptions } from '../BaseDoc-BT18xPxU.js';
5
5
  import '../json-patch/JSONPatch.js';
6
6
  import '@dabble/delta';
@@ -1,6 +1,5 @@
1
1
  import "../chunk-IZ2YBCUP.js";
2
- import { OTDoc, OTDoc as OTDoc2 } from "./OTDoc.js";
2
+ import { OTDoc } from "./OTDoc.js";
3
3
  export {
4
- OTDoc,
5
- OTDoc2 as PatchesDocClass
4
+ OTDoc
6
5
  };
@@ -1,4 +1,4 @@
1
- export { B as BaseDoc, O as OTDoc, P as PatchesDoc, O as PatchesDocClass, a as PatchesDocOptions } from '../BaseDoc-BT18xPxU.js';
1
+ export { B as BaseDoc, O as OTDoc, P as PatchesDoc, a as PatchesDocOptions } from '../BaseDoc-BT18xPxU.js';
2
2
  export { IndexedDBFactoryOptions, MultiAlgorithmFactoryOptions, MultiAlgorithmIndexedDBFactoryOptions, PatchesFactoryOptions, createLWWIndexedDBPatches, createLWWPatches, createMultiAlgorithmIndexedDBPatches, createMultiAlgorithmPatches, createOTIndexedDBPatches, createOTPatches } from './factories.js';
3
3
  export { IDBStoreWrapper, IDBTransactionWrapper, IndexedDBStore } from './IndexedDBStore.js';
4
4
  export { OTIndexedDBStore } from './OTIndexedDBStore.js';
@@ -1,4 +1,4 @@
1
- import { JSONPatchOpHandlerMap, JSONPatchOpHandler } from './json-patch/types.js';
1
+ import { JSONPatchOpHandlerMap } from './json-patch/types.js';
2
2
  import { add } from './json-patch/ops/add.js';
3
3
  import { bit } from './json-patch/ops/bitmask.js';
4
4
  import { copy } from './json-patch/ops/copy.js';
@@ -9,19 +9,7 @@ import { remove } from './json-patch/ops/remove.js';
9
9
  import { replace } from './json-patch/ops/replace.js';
10
10
  import { test } from './json-patch/ops/test.js';
11
11
 
12
- declare function getTypes(custom?: JSONPatchOpHandlerMap): {
13
- test: JSONPatchOpHandler;
14
- add: JSONPatchOpHandler;
15
- remove: JSONPatchOpHandler;
16
- replace: JSONPatchOpHandler;
17
- copy: JSONPatchOpHandler;
18
- move: JSONPatchOpHandler;
19
- '@inc': JSONPatchOpHandler;
20
- '@bit': JSONPatchOpHandler;
21
- '@txt': JSONPatchOpHandler;
22
- '@max': JSONPatchOpHandler;
23
- '@min': JSONPatchOpHandler;
24
- };
12
+ declare function getTypes(custom?: JSONPatchOpHandlerMap): JSONPatchOpHandlerMap;
25
13
 
26
14
  declare const index_add: typeof add;
27
15
  declare const index_bit: typeof bit;
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export { Delta } from '@dabble/delta';
2
- export { B as BaseDoc, O as OTDoc, P as PatchesDoc, O as PatchesDocClass, a as PatchesDocOptions } from './BaseDoc-BT18xPxU.js';
2
+ export { B as BaseDoc, O as OTDoc, P as PatchesDoc, a as PatchesDocOptions } from './BaseDoc-BT18xPxU.js';
3
3
  export { IndexedDBFactoryOptions, MultiAlgorithmFactoryOptions, MultiAlgorithmIndexedDBFactoryOptions, PatchesFactoryOptions, createLWWIndexedDBPatches, createLWWPatches, createMultiAlgorithmIndexedDBPatches, createMultiAlgorithmPatches, createOTIndexedDBPatches, createOTPatches } from './client/factories.js';
4
4
  export { IDBStoreWrapper, IDBTransactionWrapper, IndexedDBStore } from './client/IndexedDBStore.js';
5
5
  export { OTIndexedDBStore } from './client/OTIndexedDBStore.js';
@@ -18,25 +18,25 @@ export { LWWClientStore } from './client/LWWClientStore.js';
18
18
  export { ClientAlgorithm } from './client/ClientAlgorithm.js';
19
19
  export { createChange } from './data/change.js';
20
20
  export { createVersionId, createVersionMetadata } from './data/version.js';
21
- export { ReadonlyStore, Signal, SignalSubscriber, Store, Subscriber, Unsubscriber, afterChange, batch, clearAllContext, computed, readonly, signal, store, watch, whenMatches, whenReady } from 'easy-signal';
21
+ export { ReadonlyStore, Signal, SignalSubscriber, Store, Subscriber, Unsubscriber, batch, computed, readonly, signal, store, watch } from 'easy-signal';
22
22
  export { fractionalIndex, healDuplicateOrders, sortByOrder } from './fractionalIndex.js';
23
23
  export { applyPatch } from './json-patch/applyPatch.js';
24
24
  export { composePatch } from './json-patch/composePatch.js';
25
25
  export { invertPatch } from './json-patch/invertPatch.js';
26
- export { applyBitmask, bit, bitmask, combineBitmasks } from './json-patch/ops/bitmask.js';
27
- export { i as defaultOps, g as getTypes } from './index-C7ZhU2kS.js';
26
+ export { applyBitmask, bitmask, combineBitmasks } from './json-patch/ops/bitmask.js';
27
+ export { i as defaultOps } from './index-BO6EQFpw.js';
28
28
  export { createPathProxy, pathProxy } from './json-patch/pathProxy.js';
29
29
  export { transformPatch } from './json-patch/transformPatch.js';
30
30
  export { JSONPatch, PathLike, WriteOptions } from './json-patch/JSONPatch.js';
31
- export { ApplyJSONPatchOptions, JSONPatchOpHandlerMap as JSONPatchCustomTypes, JSONPatchOp } from './json-patch/types.js';
31
+ export { ApplyJSONPatchOptions, JSONPatchOp, JSONPatchOpHandlerMap } from './json-patch/types.js';
32
32
  export { Branch, BranchStatus, Change, ChangeInput, ChangeMutator, CommitChangesOptions, DeleteDocOptions, DocSyncState, DocSyncStatus, DocumentTombstone, EditableBranchMetadata, EditableVersionMetadata, ListChangesOptions, ListVersionsOptions, PatchesSnapshot, PatchesState, PathProxy, VersionMetadata } from './types.js';
33
- export { add } from './json-patch/ops/add.js';
34
- export { copy } from './json-patch/ops/copy.js';
35
- export { increment } from './json-patch/ops/increment.js';
36
- export { max, min } from './json-patch/ops/minmax.js';
37
- export { move } from './json-patch/ops/move.js';
38
- export { remove } from './json-patch/ops/remove.js';
39
- export { replace } from './json-patch/ops/replace.js';
40
- export { test } from './json-patch/ops/test.js';
41
33
  import './utils/deferred.js';
42
34
  import './net/protocol/types.js';
35
+ import './json-patch/ops/add.js';
36
+ import './json-patch/ops/copy.js';
37
+ import './json-patch/ops/increment.js';
38
+ import './json-patch/ops/minmax.js';
39
+ import './json-patch/ops/move.js';
40
+ import './json-patch/ops/remove.js';
41
+ import './json-patch/ops/replace.js';
42
+ import './json-patch/ops/test.js';
package/dist/index.js CHANGED
@@ -9,24 +9,16 @@ import {
9
9
  readonly,
10
10
  computed,
11
11
  batch,
12
- watch,
13
- whenReady,
14
- whenMatches,
15
- afterChange,
16
- clearAllContext
12
+ watch
17
13
  } from "easy-signal";
18
14
  export * from "./fractionalIndex.js";
19
15
  export * from "./json-patch/index.js";
20
16
  export {
21
17
  Delta,
22
- afterChange,
23
18
  batch,
24
- clearAllContext,
25
19
  computed,
26
20
  readonly,
27
21
  signal,
28
22
  store,
29
- watch,
30
- whenMatches,
31
- whenReady
23
+ watch
32
24
  };
@@ -1,19 +1,19 @@
1
1
  export { applyPatch } from './applyPatch.js';
2
2
  export { composePatch } from './composePatch.js';
3
3
  export { invertPatch } from './invertPatch.js';
4
- export { applyBitmask, bit, bitmask, combineBitmasks } from './ops/bitmask.js';
5
- export { i as defaultOps, g as getTypes } from '../index-C7ZhU2kS.js';
4
+ export { applyBitmask, bitmask, combineBitmasks } from './ops/bitmask.js';
5
+ export { i as defaultOps } from '../index-BO6EQFpw.js';
6
6
  export { createPathProxy, pathProxy } from './pathProxy.js';
7
7
  export { transformPatch } from './transformPatch.js';
8
8
  export { JSONPatch, PathLike, WriteOptions } from './JSONPatch.js';
9
- export { ApplyJSONPatchOptions, JSONPatchOpHandlerMap as JSONPatchCustomTypes, JSONPatchOp } from './types.js';
10
- export { add } from './ops/add.js';
11
- export { copy } from './ops/copy.js';
12
- export { increment } from './ops/increment.js';
13
- export { max, min } from './ops/minmax.js';
14
- export { move } from './ops/move.js';
15
- export { remove } from './ops/remove.js';
16
- export { replace } from './ops/replace.js';
17
- export { test } from './ops/test.js';
9
+ export { ApplyJSONPatchOptions, JSONPatchOp, JSONPatchOpHandlerMap } from './types.js';
10
+ import './ops/add.js';
11
+ import './ops/copy.js';
12
+ import './ops/increment.js';
13
+ import './ops/minmax.js';
14
+ import './ops/move.js';
15
+ import './ops/remove.js';
16
+ import './ops/replace.js';
17
+ import './ops/test.js';
18
18
  import '../types.js';
19
19
  import '@dabble/delta';
@@ -7,7 +7,6 @@ import * as defaultOps from "./ops/index.js";
7
7
  export * from "./pathProxy.js";
8
8
  import { transformPatch } from "./transformPatch.js";
9
9
  export * from "./JSONPatch.js";
10
- export * from "./ops/index.js";
11
10
  export {
12
11
  applyBitmask,
13
12
  applyPatch,
@@ -8,4 +8,4 @@ export { move } from './move.js';
8
8
  export { remove } from './remove.js';
9
9
  export { replace } from './replace.js';
10
10
  export { test } from './test.js';
11
- export { g as getTypes } from '../../index-C7ZhU2kS.js';
11
+ export { g as getTypes } from '../../index-BO6EQFpw.js';
@@ -9,21 +9,22 @@ import { remove } from "./remove.js";
9
9
  import { replace } from "./replace.js";
10
10
  import { test } from "./test.js";
11
11
  import { text } from "./text.js";
12
+ const defaultTypes = {
13
+ test,
14
+ add,
15
+ remove,
16
+ replace,
17
+ copy,
18
+ move,
19
+ "@inc": increment,
20
+ "@bit": bit,
21
+ "@txt": text,
22
+ "@max": max,
23
+ "@min": min
24
+ };
12
25
  function getTypes(custom) {
13
- return {
14
- test,
15
- add,
16
- remove,
17
- replace,
18
- copy,
19
- move,
20
- "@inc": increment,
21
- "@bit": bit,
22
- "@txt": text,
23
- "@max": max,
24
- "@min": min,
25
- ...custom
26
- };
26
+ if (!custom || Object.keys(custom).length === 0) return defaultTypes;
27
+ return { ...defaultTypes, ...custom };
27
28
  }
28
29
  export {
29
30
  add,
@@ -1,6 +1,6 @@
1
1
  import { State, JSONPatchOp, JSONPatchOpHandler } from '../types.js';
2
2
 
3
3
  declare function getType(state: State, patch: JSONPatchOp): JSONPatchOpHandler;
4
- declare function getTypeLike(state: State, patch: JSONPatchOp): "test" | "add" | "remove" | "replace" | "copy" | "move";
4
+ declare function getTypeLike(state: State, patch: JSONPatchOp): "add" | "remove" | "replace" | "move" | "copy" | "test";
5
5
 
6
6
  export { getType, getTypeLike };
@@ -0,0 +1,44 @@
1
+ import { Signal } from 'easy-signal';
2
+ import { MicroDoc } from './doc.js';
3
+ import '@dabble/delta';
4
+ import './types.js';
5
+
6
+ interface ClientOptions {
7
+ /** Base URL for REST API, e.g. "https://api.example.com" */
8
+ url: string;
9
+ /** If provided, persists state to IndexedDB with this database name. */
10
+ dbName?: string;
11
+ /** Debounce delay in ms before flushing pending ops. Default: 300 */
12
+ debounce?: number;
13
+ }
14
+ declare class MicroClient {
15
+ private _url;
16
+ private _dbName?;
17
+ private _debounce;
18
+ private _docs;
19
+ private _ws;
20
+ private _wsBackoff;
21
+ private _wsTimer;
22
+ private _db;
23
+ readonly onConnection: Signal<(connected: boolean) => void>;
24
+ constructor(opts: ClientOptions);
25
+ /** Open a document. Fetches from server (or IDB cache), subscribes via WS. */
26
+ open<T = Record<string, any>>(docId: string): Promise<MicroDoc<T>>;
27
+ /** Close a document subscription. */
28
+ close(docId: string): void;
29
+ /** Force flush pending ops for a document immediately. */
30
+ flush(docId: string): Promise<void>;
31
+ /** Disconnect WebSocket and clean up. */
32
+ destroy(): void;
33
+ private _scheduleFlush;
34
+ private _doFlush;
35
+ private _ensureWS;
36
+ private _reconnectWS;
37
+ private _wsSend;
38
+ private _fetch;
39
+ private _idbOpen;
40
+ private _idbLoad;
41
+ private _idbSave;
42
+ }
43
+
44
+ export { type ClientOptions, MicroClient };
@@ -0,0 +1,224 @@
1
+ import "../chunk-IZ2YBCUP.js";
2
+ import { signal } from "easy-signal";
3
+ import { MicroDoc } from "./doc.js";
4
+ import { transformPendingTxt } from "./ops.js";
5
+ class MicroClient {
6
+ _url;
7
+ _dbName;
8
+ _debounce;
9
+ _docs = /* @__PURE__ */ new Map();
10
+ _ws = null;
11
+ _wsBackoff = 0;
12
+ _wsTimer = null;
13
+ _db = null;
14
+ onConnection = signal();
15
+ constructor(opts) {
16
+ this._url = opts.url.replace(/\/$/, "");
17
+ this._dbName = opts.dbName;
18
+ this._debounce = opts.debounce ?? 300;
19
+ }
20
+ /** Open a document. Fetches from server (or IDB cache), subscribes via WS. */
21
+ async open(docId) {
22
+ if (this._docs.has(docId)) return this._docs.get(docId).doc;
23
+ let state = { rev: 0, fields: {} };
24
+ let pending = {};
25
+ if (this._dbName) {
26
+ const cached = await this._idbLoad(docId);
27
+ if (cached) {
28
+ state = { rev: cached.rev, fields: cached.fields };
29
+ pending = cached.pending;
30
+ }
31
+ }
32
+ try {
33
+ if (state.rev > 0 && Object.keys(pending).length) {
34
+ try {
35
+ const sync = await this._fetch(`/docs/${docId}/sync?since=${state.rev}`);
36
+ if (sync.rev > state.rev) {
37
+ Object.assign(state.fields, sync.fields);
38
+ pending = transformPendingTxt(pending, sync.textLog);
39
+ state.rev = sync.rev;
40
+ }
41
+ } catch {
42
+ const remote = await this._fetch(`/docs/${docId}`);
43
+ if (remote.rev > state.rev) {
44
+ state = remote;
45
+ pending = {};
46
+ }
47
+ }
48
+ } else {
49
+ const remote = await this._fetch(`/docs/${docId}`);
50
+ if (remote.rev > state.rev) {
51
+ state = remote;
52
+ pending = {};
53
+ }
54
+ }
55
+ } catch {
56
+ }
57
+ const doc = new MicroDoc(state.fields, pending, state.rev);
58
+ const entry = { doc, timer: null };
59
+ this._docs.set(docId, entry);
60
+ doc._onUpdate = () => this._scheduleFlush(docId);
61
+ this._ensureWS();
62
+ this._wsSend({ type: "sub", docId });
63
+ return doc;
64
+ }
65
+ /** Close a document subscription. */
66
+ close(docId) {
67
+ const entry = this._docs.get(docId);
68
+ if (!entry) return;
69
+ if (entry.timer) clearTimeout(entry.timer);
70
+ this._docs.delete(docId);
71
+ this._wsSend({ type: "unsub", docId });
72
+ }
73
+ /** Force flush pending ops for a document immediately. */
74
+ async flush(docId) {
75
+ const entry = this._docs.get(docId);
76
+ if (!entry) return;
77
+ if (entry.timer) {
78
+ clearTimeout(entry.timer);
79
+ entry.timer = null;
80
+ }
81
+ await this._doFlush(docId, entry);
82
+ }
83
+ /** Disconnect WebSocket and clean up. */
84
+ destroy() {
85
+ for (const entry of this._docs.values()) {
86
+ if (entry.timer) clearTimeout(entry.timer);
87
+ }
88
+ this._docs.clear();
89
+ if (this._wsTimer) clearTimeout(this._wsTimer);
90
+ this._ws?.close();
91
+ this._ws = null;
92
+ this._db?.close();
93
+ this._db = null;
94
+ }
95
+ // --- Sync ---
96
+ _scheduleFlush(docId) {
97
+ const entry = this._docs.get(docId);
98
+ if (!entry || entry.timer) return;
99
+ entry.timer = setTimeout(() => {
100
+ entry.timer = null;
101
+ this._doFlush(docId, entry);
102
+ }, this._debounce);
103
+ }
104
+ async _doFlush(docId, entry) {
105
+ const change = entry.doc._flush();
106
+ if (!change) return;
107
+ if (this._dbName) this._idbSave(docId, entry.doc);
108
+ try {
109
+ const result = await this._fetch(`/docs/${docId}/changes`, {
110
+ method: "POST",
111
+ headers: { "Content-Type": "application/json" },
112
+ body: JSON.stringify(change)
113
+ });
114
+ entry.doc._confirmSend(result.rev);
115
+ if (this._dbName) this._idbSave(docId, entry.doc);
116
+ if (Object.keys(entry.doc.pending).length) this._scheduleFlush(docId);
117
+ } catch {
118
+ entry.doc._failSend();
119
+ entry.timer = setTimeout(() => {
120
+ entry.timer = null;
121
+ this._doFlush(docId, entry);
122
+ }, 2e3);
123
+ }
124
+ }
125
+ // --- WebSocket ---
126
+ _ensureWS() {
127
+ if (this._ws && this._ws.readyState <= WebSocket.OPEN) return;
128
+ const wsUrl = this._url.replace(/^http/, "ws") + "/ws";
129
+ const ws = new WebSocket(wsUrl);
130
+ this._ws = ws;
131
+ ws.onopen = () => {
132
+ this._wsBackoff = 0;
133
+ this.onConnection.emit(true);
134
+ for (const docId of this._docs.keys()) {
135
+ this._wsSend({ type: "sub", docId });
136
+ }
137
+ };
138
+ ws.onmessage = (e) => {
139
+ try {
140
+ const msg = JSON.parse(e.data);
141
+ if (msg.type === "change" && msg.docId) {
142
+ const entry = this._docs.get(msg.docId);
143
+ if (entry) entry.doc.applyRemote(msg.fields, msg.rev);
144
+ }
145
+ } catch {
146
+ }
147
+ };
148
+ ws.onclose = () => {
149
+ this.onConnection.emit(false);
150
+ this._reconnectWS();
151
+ };
152
+ ws.onerror = () => ws.close();
153
+ }
154
+ _reconnectWS() {
155
+ if (this._wsTimer) return;
156
+ const delay = Math.min(1e3 * 2 ** this._wsBackoff, 3e4);
157
+ this._wsBackoff++;
158
+ this._wsTimer = setTimeout(() => {
159
+ this._wsTimer = null;
160
+ if (this._docs.size > 0) this._ensureWS();
161
+ }, delay);
162
+ }
163
+ _wsSend(msg) {
164
+ if (this._ws?.readyState === WebSocket.OPEN) {
165
+ this._ws.send(JSON.stringify(msg));
166
+ }
167
+ }
168
+ // --- REST ---
169
+ async _fetch(path, init) {
170
+ const res = await fetch(this._url + path, init);
171
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
172
+ return res.json();
173
+ }
174
+ // --- IndexedDB ---
175
+ async _idbOpen() {
176
+ if (this._db) return this._db;
177
+ return new Promise((resolve, reject) => {
178
+ const req = indexedDB.open(this._dbName, 1);
179
+ req.onupgradeneeded = () => {
180
+ const db = req.result;
181
+ if (!db.objectStoreNames.contains("docs")) db.createObjectStore("docs");
182
+ if (!db.objectStoreNames.contains("pending")) db.createObjectStore("pending");
183
+ };
184
+ req.onsuccess = () => {
185
+ this._db = req.result;
186
+ resolve(req.result);
187
+ };
188
+ req.onerror = () => reject(req.error);
189
+ });
190
+ }
191
+ async _idbLoad(docId) {
192
+ try {
193
+ const db = await this._idbOpen();
194
+ const tx = db.transaction(["docs", "pending"], "readonly");
195
+ const [docData, pendingData] = await Promise.all([
196
+ idbGet(tx.objectStore("docs"), docId),
197
+ idbGet(tx.objectStore("pending"), docId)
198
+ ]);
199
+ if (!docData) return null;
200
+ return { fields: docData.fields, rev: docData.rev, pending: pendingData?.ops ?? {} };
201
+ } catch {
202
+ return null;
203
+ }
204
+ }
205
+ async _idbSave(docId, doc) {
206
+ try {
207
+ const db = await this._idbOpen();
208
+ const tx = db.transaction(["docs", "pending"], "readwrite");
209
+ tx.objectStore("docs").put({ fields: doc.confirmed, rev: doc.rev }, docId);
210
+ tx.objectStore("pending").put({ ops: doc.pending }, docId);
211
+ } catch {
212
+ }
213
+ }
214
+ }
215
+ function idbGet(store, key) {
216
+ return new Promise((resolve, reject) => {
217
+ const req = store.get(key);
218
+ req.onsuccess = () => resolve(req.result);
219
+ req.onerror = () => reject(req.error);
220
+ });
221
+ }
222
+ export {
223
+ MicroClient
224
+ };
@@ -0,0 +1,51 @@
1
+ import { Delta } from '@dabble/delta';
2
+ import { Subscriber, Unsubscriber } from 'easy-signal';
3
+ import { FieldMap, Change } from './types.js';
4
+
5
+ interface BaseUpdates<T> {
6
+ set(val: T): void;
7
+ del(): void;
8
+ }
9
+ interface NumberUpdates extends BaseUpdates<number> {
10
+ inc(val?: number): void;
11
+ bit(val: number): void;
12
+ max(val: number): void;
13
+ }
14
+ interface StringUpdates extends BaseUpdates<string> {
15
+ max(val: string): void;
16
+ }
17
+ interface DeltaUpdates extends BaseUpdates<Delta> {
18
+ txt(delta: Delta): void;
19
+ }
20
+ type Updatable<T> = T extends Delta ? DeltaUpdates : T extends number ? NumberUpdates : T extends string ? StringUpdates : T extends object ? {
21
+ [K in keyof T]-?: Updatable<NonNullable<T[K]>>;
22
+ } & BaseUpdates<T> : BaseUpdates<T>;
23
+ declare class MicroDoc<T = Record<string, any>> {
24
+ rev: number;
25
+ private _store;
26
+ private _confirmed;
27
+ private _sending;
28
+ private _sendingId;
29
+ private _pending;
30
+ /** Called by client when ops are queued. */
31
+ _onUpdate?: () => void;
32
+ constructor(confirmed?: FieldMap, pending?: FieldMap, rev?: number);
33
+ get state(): T;
34
+ get pending(): FieldMap;
35
+ get confirmed(): FieldMap;
36
+ get isSending(): boolean;
37
+ subscribe(cb: Subscriber<T>, noInit?: false): Unsubscriber;
38
+ /** Apply changes via proxy-based updater. */
39
+ update(fn: (doc: Updatable<T>) => void): void;
40
+ /** Move pending to sending, return the Change to POST. Returns null if nothing to send. */
41
+ _flush(): Change | null;
42
+ /** Confirm a successful send. Merge sending into confirmed. */
43
+ _confirmSend(rev: number): void;
44
+ /** Roll sending back into pending on failure. */
45
+ _failSend(): void;
46
+ /** Apply remote fields from another client (via WS push). */
47
+ applyRemote(fields: FieldMap, rev: number): void;
48
+ private _rebuild;
49
+ }
50
+
51
+ export { MicroDoc, type Updatable };