@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.
- package/dist/algorithms/ot/shared/rebaseChanges.js +6 -5
- package/dist/client/LWWDoc.js +11 -24
- package/dist/client/Patches.js +2 -2
- package/dist/client/PatchesDoc.d.ts +1 -1
- package/dist/client/PatchesDoc.js +2 -3
- package/dist/client/index.d.ts +1 -1
- package/dist/{index-C7ZhU2kS.d.ts → index-BO6EQFpw.d.ts} +2 -14
- package/dist/index.d.ts +13 -13
- package/dist/index.js +2 -10
- package/dist/json-patch/index.d.ts +11 -11
- package/dist/json-patch/index.js +0 -1
- package/dist/json-patch/ops/index.d.ts +1 -1
- package/dist/json-patch/ops/index.js +15 -14
- package/dist/json-patch/utils/getType.d.ts +1 -1
- package/dist/micro/client.d.ts +44 -0
- package/dist/micro/client.js +224 -0
- package/dist/micro/doc.d.ts +51 -0
- package/dist/micro/doc.js +137 -0
- package/dist/micro/index.d.ts +7 -0
- package/dist/micro/index.js +33 -0
- package/dist/micro/ops.d.ts +22 -0
- package/dist/micro/ops.js +110 -0
- package/dist/micro/server.d.ts +47 -0
- package/dist/micro/server.js +263 -0
- package/dist/micro/types.d.ts +88 -0
- package/dist/micro/types.js +14 -0
- package/dist/net/PatchesClient.d.ts +2 -2
- package/dist/net/PatchesSync.d.ts +6 -0
- package/dist/net/PatchesSync.js +20 -8
- package/dist/net/http/FetchTransport.d.ts +1 -3
- package/dist/net/http/FetchTransport.js +5 -11
- package/dist/net/index.d.ts +2 -2
- package/dist/net/protocol/JSONRPCClient.js +7 -0
- package/dist/net/protocol/JSONRPCServer.d.ts +4 -6
- package/dist/net/protocol/JSONRPCServer.js +3 -5
- package/dist/net/protocol/types.d.ts +1 -10
- package/dist/net/rest/PatchesREST.d.ts +2 -2
- package/dist/server/LWWMemoryStoreBackend.d.ts +0 -1
- package/dist/server/LWWMemoryStoreBackend.js +0 -3
- package/dist/server/LWWServer.js +2 -1
- package/dist/server/OTBranchManager.d.ts +2 -7
- package/dist/server/OTBranchManager.js +0 -2
- package/dist/server/OTServer.js +2 -1
- package/dist/server/PatchesHistoryManager.d.ts +1 -10
- package/dist/server/PatchesHistoryManager.js +2 -18
- package/dist/server/index.d.ts +2 -2
- package/dist/server/index.js +3 -3
- package/dist/server/types.d.ts +0 -5
- package/dist/utils/concurrency.d.ts +6 -1
- package/dist/utils/concurrency.js +4 -0
- 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 =
|
|
24
|
-
|
|
23
|
+
const result = [];
|
|
24
|
+
for (const change of filteredLocalChanges) {
|
|
25
25
|
const ops = transformPatch.transform(change.ops).ops;
|
|
26
|
-
if (!ops.length)
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
if (!ops.length) continue;
|
|
27
|
+
rev++;
|
|
28
|
+
result.push({ ...change, baseRev, rev, ops });
|
|
29
|
+
}
|
|
29
30
|
return result;
|
|
30
31
|
}
|
|
31
32
|
export {
|
package/dist/client/LWWDoc.js
CHANGED
|
@@ -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
|
-
|
|
21
|
-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
101
|
-
|
|
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
|
-
|
|
110
|
-
|
|
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 {
|
package/dist/client/Patches.js
CHANGED
|
@@ -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,
|
|
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';
|
package/dist/client/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { B as BaseDoc, O as OTDoc, P as PatchesDoc,
|
|
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
|
|
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,
|
|
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,
|
|
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,
|
|
27
|
-
export { i as defaultOps
|
|
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,
|
|
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,
|
|
5
|
-
export { i as defaultOps
|
|
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,
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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';
|
package/dist/json-patch/index.js
CHANGED
|
@@ -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
|
-
|
|
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): "
|
|
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 };
|