@mengine/storage 0.1.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/auto-reconnect-0HwaV1v8.js +250 -0
- package/dist/base-B0LSXWTK.d.ts +191 -0
- package/dist/idb.d.ts +119 -0
- package/dist/idb.js +225 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +27 -0
- package/dist/pg.d.ts +671 -0
- package/dist/pg.js +241 -0
- package/package.json +58 -0
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { EventBus, MANUALLY_STOP, Task } from "@mengine/utils";
|
|
2
|
+
import { LoroDoc, VersionVector } from "loro-crdt";
|
|
3
|
+
//#region src/lock.ts
|
|
4
|
+
var SingletonLocker = class {
|
|
5
|
+
lockedResource = /* @__PURE__ */ new Map();
|
|
6
|
+
constructor() {}
|
|
7
|
+
async lock(domain, resource) {
|
|
8
|
+
const key = `${domain}:${resource}`;
|
|
9
|
+
let lock = this.lockedResource.get(key);
|
|
10
|
+
if (!lock) {
|
|
11
|
+
lock = new Lock();
|
|
12
|
+
this.lockedResource.set(key, lock);
|
|
13
|
+
}
|
|
14
|
+
await lock.acquire();
|
|
15
|
+
return lock;
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
var Lock = class {
|
|
19
|
+
inner = Promise.resolve();
|
|
20
|
+
release = () => {};
|
|
21
|
+
async acquire() {
|
|
22
|
+
let release = null;
|
|
23
|
+
const nextLock = new Promise((resolve) => {
|
|
24
|
+
release = resolve;
|
|
25
|
+
});
|
|
26
|
+
await this.inner;
|
|
27
|
+
this.inner = nextLock;
|
|
28
|
+
this.release = release;
|
|
29
|
+
}
|
|
30
|
+
[Symbol.asyncDispose]() {
|
|
31
|
+
this.release();
|
|
32
|
+
return Promise.resolve();
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
//#endregion
|
|
36
|
+
//#region src/utils.ts
|
|
37
|
+
function mergeLoroUpdates(updates) {
|
|
38
|
+
const doc = new LoroDoc();
|
|
39
|
+
for (const update of updates) doc.import(update);
|
|
40
|
+
return doc.export({ mode: "snapshot" });
|
|
41
|
+
}
|
|
42
|
+
function diffLoroUpdate(snapshot, knownVersion) {
|
|
43
|
+
const doc = new LoroDoc();
|
|
44
|
+
doc.import(snapshot);
|
|
45
|
+
return {
|
|
46
|
+
missing: doc.export({
|
|
47
|
+
mode: "update",
|
|
48
|
+
from: knownVersion ? VersionVector.decode(knownVersion) : void 0
|
|
49
|
+
}),
|
|
50
|
+
version: doc.version().encode()
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
//#endregion
|
|
54
|
+
//#region src/base.ts
|
|
55
|
+
var BaseDocStorage = class {
|
|
56
|
+
options;
|
|
57
|
+
get isReadonly() {
|
|
58
|
+
return this.options.readonlyMode ?? false;
|
|
59
|
+
}
|
|
60
|
+
storageType = "doc";
|
|
61
|
+
locker = new SingletonLocker();
|
|
62
|
+
event = new EventBus();
|
|
63
|
+
constructor(options) {
|
|
64
|
+
this.options = options;
|
|
65
|
+
}
|
|
66
|
+
async getDoc(docId) {
|
|
67
|
+
const lock = this.isReadonly ? void 0 : await this.lockDocForUpdate(docId);
|
|
68
|
+
try {
|
|
69
|
+
const snapshot = await this.getDocSnapshot(docId);
|
|
70
|
+
const updates = await this.getDocUpdates(docId);
|
|
71
|
+
if (updates.length) {
|
|
72
|
+
const newSnapshot = this.mergeUpdates(snapshot, updates);
|
|
73
|
+
if (!this.isReadonly) {
|
|
74
|
+
await this.setDocSnapshot(newSnapshot, snapshot);
|
|
75
|
+
await this.markUpdatesMerged(docId, updates);
|
|
76
|
+
}
|
|
77
|
+
return newSnapshot;
|
|
78
|
+
}
|
|
79
|
+
return snapshot;
|
|
80
|
+
} finally {
|
|
81
|
+
await lock?.[Symbol.asyncDispose]();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
async getDocDiff(docId, knownVersion) {
|
|
85
|
+
const snapshot = await this.getDoc(docId);
|
|
86
|
+
if (!snapshot) return null;
|
|
87
|
+
const { missing, version } = this.diffUpdates(snapshot.data, knownVersion);
|
|
88
|
+
return {
|
|
89
|
+
docId,
|
|
90
|
+
missing,
|
|
91
|
+
version
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
subscribeDocUpdate(callback) {
|
|
95
|
+
return this.event.on("update", ({ update, origin }) => {
|
|
96
|
+
callback(update, origin);
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Apply doc updates to a snapshot.
|
|
101
|
+
*/
|
|
102
|
+
mergeUpdates(snapshot, updates) {
|
|
103
|
+
return {
|
|
104
|
+
docId: snapshot?.docId ?? updates[0].docId,
|
|
105
|
+
data: (this.options.mergeUpdates ?? mergeLoroUpdates)([...snapshot ? [snapshot.data] : [], ...updates.map((update) => update.data)]),
|
|
106
|
+
createdAt: snapshot?.createdAt ?? /* @__PURE__ */ new Date(),
|
|
107
|
+
updatedAt: updates.at(-1)?.createdAt ?? /* @__PURE__ */ new Date()
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
diffUpdates(snapshot, knownVersion) {
|
|
111
|
+
return (this.options.diffUpdates ?? diffLoroUpdate)(snapshot, knownVersion);
|
|
112
|
+
}
|
|
113
|
+
async lockDocForUpdate(docId) {
|
|
114
|
+
return this.locker.lock(`Snapshotting`, docId);
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
//#endregion
|
|
118
|
+
//#region src/connection/auto-reconnect.ts
|
|
119
|
+
var AutoReconnectConnection = class {
|
|
120
|
+
event = new EventBus();
|
|
121
|
+
_inner = void 0;
|
|
122
|
+
_status = "idle";
|
|
123
|
+
_error = void 0;
|
|
124
|
+
retryDelay = 3e3;
|
|
125
|
+
connectingTimeout = 15e3;
|
|
126
|
+
refCount = 0;
|
|
127
|
+
connectingAbort;
|
|
128
|
+
reconnectingAbort;
|
|
129
|
+
constructor() {}
|
|
130
|
+
get shareId() {}
|
|
131
|
+
get maybeConnection() {
|
|
132
|
+
return this._inner;
|
|
133
|
+
}
|
|
134
|
+
get inner() {
|
|
135
|
+
if (this._inner === void 0) throw new Error(`Connection ${this.constructor.name} has not been established.`);
|
|
136
|
+
return this._inner;
|
|
137
|
+
}
|
|
138
|
+
set inner(inner) {
|
|
139
|
+
this._inner = inner;
|
|
140
|
+
}
|
|
141
|
+
get status() {
|
|
142
|
+
return this._status;
|
|
143
|
+
}
|
|
144
|
+
get error() {
|
|
145
|
+
return this._error;
|
|
146
|
+
}
|
|
147
|
+
set error(error) {
|
|
148
|
+
this.handleError(error);
|
|
149
|
+
}
|
|
150
|
+
setStatus(status, error) {
|
|
151
|
+
const shouldEmit = status !== this._status || error !== this._error;
|
|
152
|
+
this._status = status;
|
|
153
|
+
if (error || status === "connected") this._error = error;
|
|
154
|
+
if (shouldEmit) this.emitStatusChanged(status, this._error);
|
|
155
|
+
}
|
|
156
|
+
innerConnect() {
|
|
157
|
+
if (this.status !== "connecting") {
|
|
158
|
+
this.setStatus("connecting");
|
|
159
|
+
const connectingAbort = new AbortController();
|
|
160
|
+
this.connectingAbort = connectingAbort;
|
|
161
|
+
const signal = connectingAbort.signal;
|
|
162
|
+
const timeout = setTimeout(() => {
|
|
163
|
+
if (!signal.aborted) this.handleError(/* @__PURE__ */ new Error("connecting timeout"));
|
|
164
|
+
}, this.connectingTimeout);
|
|
165
|
+
this.doConnect(signal).then((value) => {
|
|
166
|
+
clearTimeout(timeout);
|
|
167
|
+
if (!signal.aborted) {
|
|
168
|
+
this._inner = value;
|
|
169
|
+
this.setStatus("connected");
|
|
170
|
+
} else try {
|
|
171
|
+
this.doDisconnect(value);
|
|
172
|
+
} catch (error) {
|
|
173
|
+
console.error("failed to disconnect", error);
|
|
174
|
+
}
|
|
175
|
+
}).catch((error) => {
|
|
176
|
+
if (!signal.aborted) {
|
|
177
|
+
clearTimeout(timeout);
|
|
178
|
+
console.error("failed to connect", error);
|
|
179
|
+
this.handleError(error);
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
innerDisconnect() {
|
|
185
|
+
this.connectingAbort?.abort(MANUALLY_STOP);
|
|
186
|
+
this.reconnectingAbort?.abort(MANUALLY_STOP);
|
|
187
|
+
try {
|
|
188
|
+
if (this._inner) this.doDisconnect(this._inner);
|
|
189
|
+
} catch (error) {
|
|
190
|
+
console.error("failed to disconnect", error);
|
|
191
|
+
}
|
|
192
|
+
this.reconnectingAbort = void 0;
|
|
193
|
+
this.connectingAbort = void 0;
|
|
194
|
+
this._inner = void 0;
|
|
195
|
+
}
|
|
196
|
+
handleError(reason) {
|
|
197
|
+
console.error("connection error, will reconnect", reason);
|
|
198
|
+
this.innerDisconnect();
|
|
199
|
+
if (this.status === "closed") return;
|
|
200
|
+
this.setStatus("error", reason);
|
|
201
|
+
this.reconnectingAbort = new AbortController();
|
|
202
|
+
const signal = this.reconnectingAbort.signal;
|
|
203
|
+
const timeout = setTimeout(() => {
|
|
204
|
+
if (!signal.aborted) this.innerConnect();
|
|
205
|
+
}, this.retryDelay);
|
|
206
|
+
signal.addEventListener("abort", () => {
|
|
207
|
+
clearTimeout(timeout);
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
connect() {
|
|
211
|
+
this.refCount++;
|
|
212
|
+
if (this.refCount === 1) this.innerConnect();
|
|
213
|
+
}
|
|
214
|
+
disconnect(force) {
|
|
215
|
+
if (force) this.refCount = 0;
|
|
216
|
+
else this.refCount = Math.max(this.refCount - 1, 0);
|
|
217
|
+
if (this.refCount === 0) {
|
|
218
|
+
this.innerDisconnect();
|
|
219
|
+
this.setStatus("closed");
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
waitForConnected() {
|
|
223
|
+
return new Task((resolve, _reject, scope) => {
|
|
224
|
+
if (this.status === "connected") {
|
|
225
|
+
resolve();
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
const off = this.onStatusChanged((status) => {
|
|
229
|
+
if (status === "connected") {
|
|
230
|
+
resolve();
|
|
231
|
+
off();
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
scope.disposer.add(off);
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
onStatusChanged(cb) {
|
|
238
|
+
return this.event.on("statusChanged", (data) => {
|
|
239
|
+
cb(data.status, data.error);
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
emitStatusChanged = (status, error) => {
|
|
243
|
+
this.event.emit("statusChanged", {
|
|
244
|
+
status,
|
|
245
|
+
error
|
|
246
|
+
});
|
|
247
|
+
};
|
|
248
|
+
};
|
|
249
|
+
//#endregion
|
|
250
|
+
export { BaseDocStorage as n, AutoReconnectConnection as t };
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { EventBus, Task } from "@mengine/utils";
|
|
2
|
+
|
|
3
|
+
//#region src/connection/api.d.ts
|
|
4
|
+
type ConnectionStatus = 'idle' | 'connecting' | 'connected' | 'error' | 'closed';
|
|
5
|
+
interface Connection<T = any> {
|
|
6
|
+
readonly shareId?: string;
|
|
7
|
+
readonly status: ConnectionStatus;
|
|
8
|
+
readonly error?: Error;
|
|
9
|
+
readonly inner: T;
|
|
10
|
+
connect(): void;
|
|
11
|
+
disconnect(): void;
|
|
12
|
+
waitForConnected(): Task<void>;
|
|
13
|
+
onStatusChanged(cb: (status: ConnectionStatus, error?: Error) => void): () => void;
|
|
14
|
+
}
|
|
15
|
+
declare class DummyConnection implements Connection<undefined> {
|
|
16
|
+
readonly status: ConnectionStatus;
|
|
17
|
+
readonly inner: undefined;
|
|
18
|
+
connect(): void;
|
|
19
|
+
disconnect(): void;
|
|
20
|
+
waitForConnected(): Task<void>;
|
|
21
|
+
onStatusChanged(_cb: (status: ConnectionStatus, error?: Error) => void): () => void;
|
|
22
|
+
}
|
|
23
|
+
//#endregion
|
|
24
|
+
//#region src/connection/auto-reconnect.d.ts
|
|
25
|
+
declare abstract class AutoReconnectConnection<T = any> implements Connection<T> {
|
|
26
|
+
private readonly event;
|
|
27
|
+
private _inner;
|
|
28
|
+
private _status;
|
|
29
|
+
private _error;
|
|
30
|
+
retryDelay: number;
|
|
31
|
+
connectingTimeout: number;
|
|
32
|
+
private refCount;
|
|
33
|
+
private connectingAbort?;
|
|
34
|
+
private reconnectingAbort?;
|
|
35
|
+
constructor();
|
|
36
|
+
get shareId(): string | undefined;
|
|
37
|
+
get maybeConnection(): T | undefined;
|
|
38
|
+
get inner(): T;
|
|
39
|
+
private set inner(value);
|
|
40
|
+
get status(): ConnectionStatus;
|
|
41
|
+
get error(): Error | undefined;
|
|
42
|
+
protected set error(error: Error | undefined);
|
|
43
|
+
private setStatus;
|
|
44
|
+
protected abstract doConnect(signal?: AbortSignal): Promise<T>;
|
|
45
|
+
protected abstract doDisconnect(conn: T): void;
|
|
46
|
+
private innerConnect;
|
|
47
|
+
private innerDisconnect;
|
|
48
|
+
private handleError;
|
|
49
|
+
connect(): void;
|
|
50
|
+
disconnect(force?: boolean): void;
|
|
51
|
+
waitForConnected(): Task<void>;
|
|
52
|
+
onStatusChanged(cb: (status: ConnectionStatus, error?: Error) => void): () => void;
|
|
53
|
+
private readonly emitStatusChanged;
|
|
54
|
+
}
|
|
55
|
+
//#endregion
|
|
56
|
+
//#region src/connection/shared-connection.d.ts
|
|
57
|
+
declare function share<T extends Connection>(conn: T): T;
|
|
58
|
+
//#endregion
|
|
59
|
+
//#region src/api.d.ts
|
|
60
|
+
interface DocSnapshotRecord {
|
|
61
|
+
docId: string;
|
|
62
|
+
data: Uint8Array;
|
|
63
|
+
createdAt: Date;
|
|
64
|
+
updatedAt: Date;
|
|
65
|
+
}
|
|
66
|
+
interface DocDiff {
|
|
67
|
+
docId: string;
|
|
68
|
+
version: Uint8Array;
|
|
69
|
+
missing: Uint8Array;
|
|
70
|
+
}
|
|
71
|
+
interface DocUpdate {
|
|
72
|
+
docId: string;
|
|
73
|
+
data: Uint8Array;
|
|
74
|
+
}
|
|
75
|
+
interface DocUpdateRecord extends DocUpdate {
|
|
76
|
+
seq: number;
|
|
77
|
+
createdAt: Date;
|
|
78
|
+
}
|
|
79
|
+
interface DocStorageOptions {
|
|
80
|
+
/**
|
|
81
|
+
* open as readonly mode.
|
|
82
|
+
*/
|
|
83
|
+
readonlyMode?: boolean;
|
|
84
|
+
/**
|
|
85
|
+
* Merge updates into a single update(snapshot).
|
|
86
|
+
*/
|
|
87
|
+
mergeUpdates?: (updates: Uint8Array[]) => Uint8Array;
|
|
88
|
+
/**
|
|
89
|
+
* Diff the snapshot with the given version.
|
|
90
|
+
*/
|
|
91
|
+
diffUpdates?: (snapshot: Uint8Array, knownVersion?: Uint8Array) => {
|
|
92
|
+
missing: Uint8Array;
|
|
93
|
+
version: Uint8Array;
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
interface DocStorage {
|
|
97
|
+
readonly isReadonly: boolean;
|
|
98
|
+
readonly connection: Connection;
|
|
99
|
+
/**
|
|
100
|
+
* Get a doc record with latest binary.
|
|
101
|
+
*/
|
|
102
|
+
getDoc(docId: string): Promise<DocSnapshotRecord | null>;
|
|
103
|
+
/**
|
|
104
|
+
* Get a diff with the given state vector.
|
|
105
|
+
*/
|
|
106
|
+
getDocDiff(docId: string, knownVersion?: Uint8Array): Promise<DocDiff | null>;
|
|
107
|
+
/**
|
|
108
|
+
* Push updates into storage
|
|
109
|
+
*/
|
|
110
|
+
pushDocUpdate(update: DocUpdate, origin: unknown): Promise<void>;
|
|
111
|
+
/**
|
|
112
|
+
* Delete a specific doc data with all snapshots and updates
|
|
113
|
+
*/
|
|
114
|
+
deleteDoc(docId: string): Promise<void>;
|
|
115
|
+
/**
|
|
116
|
+
* Subscribe to doc updates
|
|
117
|
+
*/
|
|
118
|
+
subscribeDocUpdate(callback: (update: DocUpdate, origin: unknown) => void): () => void;
|
|
119
|
+
}
|
|
120
|
+
//#endregion
|
|
121
|
+
//#region src/lock.d.ts
|
|
122
|
+
interface Locker {
|
|
123
|
+
lock(domain: string, resource: string): Promise<AsyncDisposable>;
|
|
124
|
+
}
|
|
125
|
+
//#endregion
|
|
126
|
+
//#region src/base.d.ts
|
|
127
|
+
type DocStorageEvents = {
|
|
128
|
+
update: [{
|
|
129
|
+
update: DocUpdate;
|
|
130
|
+
origin: unknown;
|
|
131
|
+
}];
|
|
132
|
+
};
|
|
133
|
+
declare abstract class BaseDocStorage<Opts = {}> implements DocStorage {
|
|
134
|
+
protected readonly options: Opts & DocStorageOptions;
|
|
135
|
+
get isReadonly(): boolean;
|
|
136
|
+
abstract readonly connection: Connection;
|
|
137
|
+
readonly storageType = "doc";
|
|
138
|
+
protected locker: Locker;
|
|
139
|
+
protected readonly event: EventBus<DocStorageEvents>;
|
|
140
|
+
constructor(options: Opts & DocStorageOptions);
|
|
141
|
+
getDoc(docId: string): Promise<DocSnapshotRecord | null>;
|
|
142
|
+
getDocDiff(docId: string, knownVersion?: Uint8Array): Promise<DocDiff | null>;
|
|
143
|
+
abstract pushDocUpdate(update: DocUpdate, origin: unknown): Promise<void>;
|
|
144
|
+
abstract deleteDoc(docId: string): Promise<void>;
|
|
145
|
+
subscribeDocUpdate(callback: (update: DocUpdate, origin: unknown) => void): () => void;
|
|
146
|
+
/**
|
|
147
|
+
* Get a doc snapshot from storage
|
|
148
|
+
*/
|
|
149
|
+
protected abstract getDocSnapshot(docId: string): Promise<DocSnapshotRecord | null>;
|
|
150
|
+
/**
|
|
151
|
+
* Set the doc snapshot into storage
|
|
152
|
+
*
|
|
153
|
+
* @safety
|
|
154
|
+
* be careful when implementing this method.
|
|
155
|
+
*
|
|
156
|
+
* It might be called with outdated snapshot when running in multi-thread environment.
|
|
157
|
+
*
|
|
158
|
+
* A common solution is update the snapshot record is DB only when the coming one's timestamp is newer.
|
|
159
|
+
*
|
|
160
|
+
* @example
|
|
161
|
+
* ```ts
|
|
162
|
+
* await using _lock = await this.lockDocForUpdate(docId);
|
|
163
|
+
* // set snapshot
|
|
164
|
+
*
|
|
165
|
+
* ```
|
|
166
|
+
*/
|
|
167
|
+
protected abstract setDocSnapshot(snapshot: DocSnapshotRecord, prevSnapshot: DocSnapshotRecord | null): Promise<boolean>;
|
|
168
|
+
/**
|
|
169
|
+
* Get all updates of a doc that haven't been merged into snapshot.
|
|
170
|
+
*
|
|
171
|
+
* Updates queue design exists for a performance concern:
|
|
172
|
+
* A huge amount of write time will be saved if we don't merge updates into snapshot immediately.
|
|
173
|
+
* Updates will be merged into snapshot when the latest doc is requested.
|
|
174
|
+
*/
|
|
175
|
+
protected abstract getDocUpdates(docId: string): Promise<DocUpdateRecord[]>;
|
|
176
|
+
/**
|
|
177
|
+
* Mark updates as merged into snapshot.
|
|
178
|
+
*/
|
|
179
|
+
protected abstract markUpdatesMerged(docId: string, updates: DocUpdateRecord[]): Promise<number>;
|
|
180
|
+
/**
|
|
181
|
+
* Apply doc updates to a snapshot.
|
|
182
|
+
*/
|
|
183
|
+
protected mergeUpdates(snapshot: DocSnapshotRecord | null, updates: DocUpdateRecord[]): DocSnapshotRecord;
|
|
184
|
+
protected diffUpdates(snapshot: Uint8Array, knownVersion?: Uint8Array): {
|
|
185
|
+
missing: Uint8Array;
|
|
186
|
+
version: Uint8Array;
|
|
187
|
+
};
|
|
188
|
+
protected lockDocForUpdate(docId: string): Promise<AsyncDisposable>;
|
|
189
|
+
}
|
|
190
|
+
//#endregion
|
|
191
|
+
export { DocStorage as a, DocUpdateRecord as c, Connection as d, ConnectionStatus as f, DocSnapshotRecord as i, share as l, Locker as n, DocStorageOptions as o, DummyConnection as p, DocDiff as r, DocUpdate as s, BaseDocStorage as t, AutoReconnectConnection as u };
|
package/dist/idb.d.ts
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { c as DocUpdateRecord, i as DocSnapshotRecord, n as Locker, s as DocUpdate, t as BaseDocStorage, u as AutoReconnectConnection } from "./base-B0LSXWTK.js";
|
|
2
|
+
import { DBSchema, IDBPDatabase } from "idb";
|
|
3
|
+
|
|
4
|
+
//#region src/impls/idb/schema.d.ts
|
|
5
|
+
/**
|
|
6
|
+
IndexedDB
|
|
7
|
+
> DB(medeo-engine)
|
|
8
|
+
> Table(Snapshots)
|
|
9
|
+
> Table(Updates)
|
|
10
|
+
|
|
11
|
+
Table(Snapshots)
|
|
12
|
+
| docId | data | seq | createdAt | updatedAt |
|
|
13
|
+
|-------|------|-----|-----------|-----------|
|
|
14
|
+
| str | bin | num | Date | Date |
|
|
15
|
+
|
|
16
|
+
Table(Updates)
|
|
17
|
+
| id | docId | data | createdAt |
|
|
18
|
+
|----|-------|------|-----------|
|
|
19
|
+
|auto| str | bin | Date |
|
|
20
|
+
|
|
21
|
+
Table(Locks)
|
|
22
|
+
| key | lock |
|
|
23
|
+
|-----|------|
|
|
24
|
+
| str | Date |
|
|
25
|
+
*/
|
|
26
|
+
interface DocStorageSchema extends DBSchema {
|
|
27
|
+
snapshots: {
|
|
28
|
+
key: string;
|
|
29
|
+
value: {
|
|
30
|
+
docId: string;
|
|
31
|
+
data: Uint8Array;
|
|
32
|
+
createdAt: Date;
|
|
33
|
+
updatedAt: Date;
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
updates: {
|
|
37
|
+
key: [string, number];
|
|
38
|
+
value: {
|
|
39
|
+
docId: string;
|
|
40
|
+
seq: number;
|
|
41
|
+
data: Uint8Array;
|
|
42
|
+
createdAt: Date;
|
|
43
|
+
};
|
|
44
|
+
indexes: {
|
|
45
|
+
docId: string;
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
sequences: {
|
|
49
|
+
key: string;
|
|
50
|
+
value: {
|
|
51
|
+
docId: string;
|
|
52
|
+
seq: number;
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
locks: {
|
|
56
|
+
key: string;
|
|
57
|
+
value: {
|
|
58
|
+
key: string;
|
|
59
|
+
lockedAt: Date;
|
|
60
|
+
};
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
//#endregion
|
|
64
|
+
//#region src/impls/idb/connection.d.ts
|
|
65
|
+
declare class IDBConnection extends AutoReconnectConnection<{
|
|
66
|
+
db: IDBPDatabase<DocStorageSchema>;
|
|
67
|
+
channel: BroadcastChannel;
|
|
68
|
+
}> {
|
|
69
|
+
readonly dbName: string;
|
|
70
|
+
constructor(dbName: string);
|
|
71
|
+
get shareId(): string;
|
|
72
|
+
doConnect(): Promise<{
|
|
73
|
+
db: IDBPDatabase<DocStorageSchema>;
|
|
74
|
+
channel: BroadcastChannel;
|
|
75
|
+
}>;
|
|
76
|
+
doDisconnect(db: {
|
|
77
|
+
db: IDBPDatabase<DocStorageSchema>;
|
|
78
|
+
channel: BroadcastChannel;
|
|
79
|
+
}): void;
|
|
80
|
+
handleVersionChange: (e: IDBVersionChangeEvent) => void;
|
|
81
|
+
}
|
|
82
|
+
//#endregion
|
|
83
|
+
//#region src/impls/idb/lock.d.ts
|
|
84
|
+
declare class IndexedDBLocker implements Locker {
|
|
85
|
+
private readonly dbConnection;
|
|
86
|
+
get db(): import("idb").IDBPDatabase<DocStorageSchema>;
|
|
87
|
+
get channel(): BroadcastChannel;
|
|
88
|
+
constructor(dbConnection: IDBConnection);
|
|
89
|
+
lock(domain: string, resource: string): Promise<{
|
|
90
|
+
[Symbol.asyncDispose]: () => Promise<void>;
|
|
91
|
+
}>;
|
|
92
|
+
}
|
|
93
|
+
//#endregion
|
|
94
|
+
//#region src/impls/idb/index.d.ts
|
|
95
|
+
interface ChannelMessage {
|
|
96
|
+
type: 'update';
|
|
97
|
+
update: DocUpdate;
|
|
98
|
+
origin?: unknown;
|
|
99
|
+
}
|
|
100
|
+
declare class IndexedDBDocStorage extends BaseDocStorage<{
|
|
101
|
+
dbName: string;
|
|
102
|
+
}> {
|
|
103
|
+
static readonly identifier = "IndexedDBDocStorage";
|
|
104
|
+
readonly connection: IDBConnection;
|
|
105
|
+
protected readonly locker: IndexedDBLocker;
|
|
106
|
+
get db(): import("idb").IDBPDatabase<DocStorageSchema>;
|
|
107
|
+
get channel(): BroadcastChannel;
|
|
108
|
+
pushDocUpdate(update: DocUpdate, origin: unknown): Promise<void>;
|
|
109
|
+
protected getDocSnapshot(docId: string): Promise<DocSnapshotRecord | null>;
|
|
110
|
+
deleteDoc(docId: string): Promise<void>;
|
|
111
|
+
protected setDocSnapshot(snapshot: DocSnapshotRecord): Promise<boolean>;
|
|
112
|
+
protected getDocUpdates(docId: string): Promise<DocUpdateRecord[]>;
|
|
113
|
+
protected markUpdatesMerged(docId: string, updates: DocUpdateRecord[]): Promise<number>;
|
|
114
|
+
private docUpdateListener;
|
|
115
|
+
subscribeDocUpdate(callback: (update: DocUpdate, origin: unknown) => void): () => void;
|
|
116
|
+
handleChannelMessage: (event: MessageEvent<ChannelMessage>) => void;
|
|
117
|
+
}
|
|
118
|
+
//#endregion
|
|
119
|
+
export { IndexedDBDocStorage };
|