@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/idb.js ADDED
@@ -0,0 +1,225 @@
1
+ import { n as BaseDocStorage, t as AutoReconnectConnection } from "./auto-reconnect-0HwaV1v8.js";
2
+ import { Task } from "@mengine/utils";
3
+ import { openDB } from "idb";
4
+ //#region src/impls/idb/schema.ts
5
+ const migrate = (db, oldVersion, _newVersion, trx) => {
6
+ if (!oldVersion) oldVersion = 0;
7
+ for (let i = oldVersion; i < migrations.length; i++) migrations[i](db, trx);
8
+ };
9
+ const init = (db) => {
10
+ db.createObjectStore("snapshots", {
11
+ keyPath: "docId",
12
+ autoIncrement: false
13
+ });
14
+ db.createObjectStore("updates", {
15
+ keyPath: ["docId", "seq"],
16
+ autoIncrement: false
17
+ }).createIndex("docId", "docId", { unique: false });
18
+ db.createObjectStore("sequences", {
19
+ keyPath: "docId",
20
+ autoIncrement: false
21
+ });
22
+ db.createObjectStore("locks", {
23
+ keyPath: "key",
24
+ autoIncrement: false
25
+ });
26
+ };
27
+ const migrations = [init];
28
+ const migrator = {
29
+ version: migrations.length,
30
+ migrate
31
+ };
32
+ //#endregion
33
+ //#region src/impls/idb/connection.ts
34
+ var IDBConnection = class extends AutoReconnectConnection {
35
+ dbName;
36
+ constructor(dbName) {
37
+ super();
38
+ this.dbName = dbName;
39
+ }
40
+ get shareId() {
41
+ return `idb(${migrator.version}):${this.dbName}`;
42
+ }
43
+ async doConnect() {
44
+ const db = await openDB(this.dbName, migrator.version, { upgrade: migrator.migrate });
45
+ db.addEventListener("versionchange", this.handleVersionChange);
46
+ return {
47
+ db,
48
+ channel: new BroadcastChannel("idb:" + this.dbName)
49
+ };
50
+ }
51
+ doDisconnect(db) {
52
+ db.db.removeEventListener("versionchange", this.handleVersionChange);
53
+ db.channel.close();
54
+ db.db.close();
55
+ }
56
+ handleVersionChange = (e) => {
57
+ if (e.newVersion !== migrator.version) this.error = /* @__PURE__ */ new Error("Database version mismatch, expected " + migrator.version + " but got " + e.newVersion);
58
+ };
59
+ };
60
+ //#endregion
61
+ //#region src/impls/idb/lock.ts
62
+ var IndexedDBLocker = class {
63
+ dbConnection;
64
+ get db() {
65
+ return this.dbConnection.inner.db;
66
+ }
67
+ get channel() {
68
+ return this.dbConnection.inner.channel;
69
+ }
70
+ constructor(dbConnection) {
71
+ this.dbConnection = dbConnection;
72
+ }
73
+ async lock(domain, resource) {
74
+ const key = `${domain}:${resource}`;
75
+ return Task.spawn(async () => {
76
+ while (true) {
77
+ const trx = this.db.transaction("locks", "readwrite");
78
+ const record = await trx.store.get(key);
79
+ if (record) {
80
+ if (record.lockedAt.getTime() < Date.now() - 3e3) {
81
+ await trx.store.delete(key);
82
+ trx.commit();
83
+ await trx.done;
84
+ } else trx.abort();
85
+ continue;
86
+ }
87
+ await trx.store.put({
88
+ key,
89
+ lockedAt: /* @__PURE__ */ new Date()
90
+ });
91
+ trx.commit();
92
+ await trx.done;
93
+ return { [Symbol.asyncDispose]: async () => {
94
+ const trx = this.db.transaction("locks", "readwrite");
95
+ await trx.store.delete(key);
96
+ trx.commit();
97
+ await trx.done;
98
+ } };
99
+ }
100
+ }).timeout(3e3);
101
+ }
102
+ };
103
+ //#endregion
104
+ //#region src/impls/idb/index.ts
105
+ var IndexedDBDocStorage = class extends BaseDocStorage {
106
+ static identifier = "IndexedDBDocStorage";
107
+ connection = new IDBConnection(this.options.dbName);
108
+ locker = new IndexedDBLocker(this.connection);
109
+ get db() {
110
+ return this.connection.inner.db;
111
+ }
112
+ get channel() {
113
+ return this.connection.inner.channel;
114
+ }
115
+ async pushDocUpdate(update, origin) {
116
+ const trx = this.db.transaction([
117
+ "snapshots",
118
+ "updates",
119
+ "sequences"
120
+ ], "readwrite");
121
+ if (!await trx.objectStore("snapshots").get(update.docId)) {
122
+ await trx.objectStore("snapshots").put({
123
+ docId: update.docId,
124
+ data: update.data,
125
+ createdAt: /* @__PURE__ */ new Date(),
126
+ updatedAt: /* @__PURE__ */ new Date()
127
+ });
128
+ return;
129
+ }
130
+ let seq = await trx.objectStore("sequences").get(update.docId);
131
+ if (!seq) seq = {
132
+ docId: update.docId,
133
+ seq: 0
134
+ };
135
+ seq.seq++;
136
+ await trx.objectStore("sequences").put(seq);
137
+ await trx.objectStore("updates").put({
138
+ docId: update.docId,
139
+ seq: seq.seq,
140
+ data: update.data,
141
+ createdAt: /* @__PURE__ */ new Date()
142
+ });
143
+ this.event.emit("update", {
144
+ update: {
145
+ docId: update.docId,
146
+ data: update.data
147
+ },
148
+ origin
149
+ });
150
+ this.channel.postMessage({
151
+ type: "update",
152
+ update: {
153
+ docId: update.docId,
154
+ data: update.data
155
+ },
156
+ origin
157
+ });
158
+ }
159
+ async getDocSnapshot(docId) {
160
+ return await this.db.transaction("snapshots", "readonly").store.get(docId) ?? null;
161
+ }
162
+ async deleteDoc(docId) {
163
+ {
164
+ const trx = this.db.transaction(["updates"], "readwrite");
165
+ const iter = trx.objectStore("updates").index("docId").iterate(IDBKeyRange.only(docId));
166
+ for await (const { primaryKey } of iter) await trx.objectStore("updates").delete(primaryKey);
167
+ trx.commit();
168
+ await trx.done;
169
+ }
170
+ {
171
+ const trx = this.db.transaction(["snapshots"], "readwrite");
172
+ await trx.objectStore("snapshots").delete(docId);
173
+ trx.commit();
174
+ }
175
+ await this.db.delete("locks", docId);
176
+ await this.db.delete("sequences", docId);
177
+ }
178
+ async setDocSnapshot(snapshot) {
179
+ const trx = this.db.transaction("snapshots", "readwrite");
180
+ const record = await trx.store.get(snapshot.docId);
181
+ if (!record || record.updatedAt <= snapshot.updatedAt) await trx.store.put({
182
+ docId: snapshot.docId,
183
+ data: snapshot.data,
184
+ createdAt: record?.createdAt ?? snapshot.createdAt,
185
+ updatedAt: snapshot.updatedAt
186
+ });
187
+ trx.commit();
188
+ await trx.done;
189
+ return true;
190
+ }
191
+ async getDocUpdates(docId) {
192
+ return (await this.db.transaction("updates", "readonly").store.index("docId").getAll(docId)).map((update) => ({
193
+ docId,
194
+ seq: update.seq,
195
+ data: update.data,
196
+ createdAt: update.createdAt
197
+ }));
198
+ }
199
+ async markUpdatesMerged(docId, updates) {
200
+ const trx = this.db.transaction("updates", "readwrite");
201
+ await Promise.all(updates.map((update) => trx.store.delete([docId, update.seq])));
202
+ trx.commit();
203
+ await trx.done;
204
+ return updates.length;
205
+ }
206
+ docUpdateListener = 0;
207
+ subscribeDocUpdate(callback) {
208
+ if (this.docUpdateListener === 0) this.channel.addEventListener("message", this.handleChannelMessage);
209
+ this.docUpdateListener++;
210
+ const dispose = super.subscribeDocUpdate(callback);
211
+ return () => {
212
+ dispose();
213
+ this.docUpdateListener--;
214
+ if (this.docUpdateListener === 0) this.channel.removeEventListener("message", this.handleChannelMessage);
215
+ };
216
+ }
217
+ handleChannelMessage = (event) => {
218
+ if (event.data.type === "update") this.event.emit("update", {
219
+ update: event.data.update,
220
+ origin: event.data.origin
221
+ });
222
+ };
223
+ };
224
+ //#endregion
225
+ export { IndexedDBDocStorage };
@@ -0,0 +1,2 @@
1
+ import { a as DocStorage, c as DocUpdateRecord, d as Connection, f as ConnectionStatus, i as DocSnapshotRecord, l as share, o as DocStorageOptions, p as DummyConnection, r as DocDiff, s as DocUpdate, t as BaseDocStorage, u as AutoReconnectConnection } from "./base-B0LSXWTK.js";
2
+ export { AutoReconnectConnection, BaseDocStorage, Connection, ConnectionStatus, DocDiff, DocSnapshotRecord, DocStorage, DocStorageOptions, DocUpdate, DocUpdateRecord, DummyConnection, share };
package/dist/index.js ADDED
@@ -0,0 +1,27 @@
1
+ import { n as BaseDocStorage, t as AutoReconnectConnection } from "./auto-reconnect-0HwaV1v8.js";
2
+ import { Task } from "@mengine/utils";
3
+ //#region src/connection/api.ts
4
+ var DummyConnection = class {
5
+ status = "connected";
6
+ inner;
7
+ connect() {}
8
+ disconnect() {}
9
+ waitForConnected() {
10
+ return Task.resolve();
11
+ }
12
+ onStatusChanged(_cb) {
13
+ return () => {};
14
+ }
15
+ };
16
+ //#endregion
17
+ //#region src/connection/shared-connection.ts
18
+ const CONNECTIONS = /* @__PURE__ */ new Map();
19
+ function share(conn) {
20
+ if (!conn.shareId) throw new Error(`Connection ${conn.constructor.name} is not shareable.\nIf you want to make it shareable, please override [shareId].`);
21
+ const existing = CONNECTIONS.get(conn.shareId);
22
+ if (existing) return existing;
23
+ CONNECTIONS.set(conn.shareId, conn);
24
+ return conn;
25
+ }
26
+ //#endregion
27
+ export { AutoReconnectConnection, BaseDocStorage, DummyConnection, share };