@palantir/pack.state.demo 0.1.1
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/.turbo/turbo-lint.log +4 -0
- package/.turbo/turbo-transpileBrowser.log +6 -0
- package/.turbo/turbo-transpileCjs.log +6 -0
- package/.turbo/turbo-transpileEsm.log +6 -0
- package/.turbo/turbo-transpileTypes.log +5 -0
- package/.turbo/turbo-typecheck.log +4 -0
- package/CHANGELOG.md +39 -0
- package/LICENSE.txt +13 -0
- package/README.md +110 -0
- package/build/browser/index.js +545 -0
- package/build/browser/index.js.map +1 -0
- package/build/cjs/index.cjs +570 -0
- package/build/cjs/index.cjs.map +1 -0
- package/build/cjs/index.d.cts +88 -0
- package/build/esm/index.js +545 -0
- package/build/esm/index.js.map +1 -0
- package/build/types/DemoDocumentService.d.ts +40 -0
- package/build/types/DemoDocumentService.d.ts.map +1 -0
- package/build/types/MetadataStore.d.ts +20 -0
- package/build/types/MetadataStore.d.ts.map +1 -0
- package/build/types/PresenceManager.d.ts +27 -0
- package/build/types/PresenceManager.d.ts.map +1 -0
- package/build/types/__tests__/DemoDocumentService.test.d.ts +1 -0
- package/build/types/__tests__/DemoDocumentService.test.d.ts.map +1 -0
- package/build/types/index.d.ts +7 -0
- package/build/types/index.d.ts.map +1 -0
- package/package.json +74 -0
- package/src/DemoDocumentService.ts +414 -0
- package/src/MetadataStore.ts +100 -0
- package/src/PresenceManager.ts +323 -0
- package/src/__tests__/DemoDocumentService.test.ts +414 -0
- package/src/index.ts +33 -0
- package/tsconfig.json +21 -0
- package/vitest.config.mjs +28 -0
- package/vitest.setup.ts +1 -0
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2025 Palantir Technologies, Inc. All rights reserved.
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { PackAppInternal, Unsubscribe } from "@palantir/pack.core";
|
|
18
|
+
import { generateId, getOntologyRid } from "@palantir/pack.core";
|
|
19
|
+
import type {
|
|
20
|
+
ActivityEvent,
|
|
21
|
+
ActivityEventId,
|
|
22
|
+
DocumentId,
|
|
23
|
+
DocumentMetadata,
|
|
24
|
+
DocumentRef,
|
|
25
|
+
DocumentSchema,
|
|
26
|
+
EditDescription,
|
|
27
|
+
Model,
|
|
28
|
+
ModelData,
|
|
29
|
+
PresenceEvent,
|
|
30
|
+
UserId,
|
|
31
|
+
} from "@palantir/pack.document-schema.model-types";
|
|
32
|
+
import {
|
|
33
|
+
ActivityEventDataType,
|
|
34
|
+
getMetadata,
|
|
35
|
+
hasMetadata,
|
|
36
|
+
} from "@palantir/pack.document-schema.model-types";
|
|
37
|
+
import type {
|
|
38
|
+
CreateDocumentMetadata,
|
|
39
|
+
InternalYjsDoc,
|
|
40
|
+
SearchDocumentsResult,
|
|
41
|
+
} from "@palantir/pack.state.core";
|
|
42
|
+
import {
|
|
43
|
+
BaseYjsDocumentService,
|
|
44
|
+
createDocRef,
|
|
45
|
+
DocumentLiveStatus,
|
|
46
|
+
DocumentLoadStatus,
|
|
47
|
+
} from "@palantir/pack.state.core";
|
|
48
|
+
import { Base64 } from "js-base64";
|
|
49
|
+
import { IndexeddbPersistence } from "y-indexeddb";
|
|
50
|
+
import * as Y from "yjs";
|
|
51
|
+
import { MetadataStore } from "./MetadataStore.js";
|
|
52
|
+
import { PresenceManager } from "./PresenceManager.js";
|
|
53
|
+
|
|
54
|
+
const EMPTY_DOCUMENT_SECURITY = Object.freeze({
|
|
55
|
+
discretionary: {},
|
|
56
|
+
mandatory: {},
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const CLIENT_ID_STORAGE_KEY = "pack-demo-client-id";
|
|
60
|
+
|
|
61
|
+
function getOrCreateClientId(): string {
|
|
62
|
+
try {
|
|
63
|
+
const storedId = sessionStorage.getItem(CLIENT_ID_STORAGE_KEY);
|
|
64
|
+
|
|
65
|
+
if (storedId != null) {
|
|
66
|
+
return storedId;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const newId = crypto.randomUUID();
|
|
70
|
+
|
|
71
|
+
sessionStorage.setItem(CLIENT_ID_STORAGE_KEY, newId);
|
|
72
|
+
|
|
73
|
+
return newId;
|
|
74
|
+
} catch {
|
|
75
|
+
return crypto.randomUUID();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface DemoDocumentServiceOptions {
|
|
80
|
+
readonly dbPrefix?: string;
|
|
81
|
+
readonly clearOnInit?: boolean;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
interface DemoInternalDoc extends InternalYjsDoc {
|
|
85
|
+
channel?: BroadcastChannel;
|
|
86
|
+
presenceManager?: PresenceManager;
|
|
87
|
+
provider?: IndexeddbPersistence;
|
|
88
|
+
updateHandler?: (update: Uint8Array, origin: unknown) => void;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export class DemoDocumentService extends BaseYjsDocumentService<DemoInternalDoc> {
|
|
92
|
+
private readonly clientId: string;
|
|
93
|
+
private readonly dbPrefix: string;
|
|
94
|
+
private readonly metadataStore: MetadataStore;
|
|
95
|
+
|
|
96
|
+
constructor(app: PackAppInternal, options: DemoDocumentServiceOptions = {}) {
|
|
97
|
+
super(app, app.config.logger.child({}, { level: "debug", msgPrefix: "DemoDocumentService" }), {
|
|
98
|
+
isDemo: true,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
this.clientId = getOrCreateClientId();
|
|
102
|
+
this.dbPrefix = options.dbPrefix ?? "pack-demo";
|
|
103
|
+
this.metadataStore = new MetadataStore(this.dbPrefix);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
override createInternalDoc(
|
|
107
|
+
ref: DocumentRef,
|
|
108
|
+
metadata?: DocumentMetadata,
|
|
109
|
+
): DemoInternalDoc {
|
|
110
|
+
const internalDoc = this.createBaseInternalDoc(ref, metadata) as DemoInternalDoc;
|
|
111
|
+
this.ensureUpdateHandler(internalDoc, ref);
|
|
112
|
+
return internalDoc;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
private ensureUpdateHandler(internalDoc: DemoInternalDoc, docRef: DocumentRef): void {
|
|
116
|
+
if (internalDoc.updateHandler) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const channel = new BroadcastChannel(`pack-demo-doc-${docRef.id}`);
|
|
121
|
+
internalDoc.channel = channel;
|
|
122
|
+
|
|
123
|
+
const updateHandler = (update: Uint8Array, origin: unknown) => {
|
|
124
|
+
if (origin === "remote") return;
|
|
125
|
+
|
|
126
|
+
if (isEditDescription(origin)) {
|
|
127
|
+
if (!internalDoc.presenceManager) {
|
|
128
|
+
internalDoc.presenceManager = new PresenceManager(
|
|
129
|
+
docRef.id,
|
|
130
|
+
this.clientId,
|
|
131
|
+
docRef.schema,
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const modelName = getMetadata(origin.model).name;
|
|
136
|
+
const event: ActivityEvent = {
|
|
137
|
+
aggregationKey: `${docRef.id}-${modelName}`,
|
|
138
|
+
createdBy: this.clientId as UserId,
|
|
139
|
+
createdInstant: Date.now(),
|
|
140
|
+
eventData: {
|
|
141
|
+
eventData: origin.data,
|
|
142
|
+
model: origin.model,
|
|
143
|
+
type: ActivityEventDataType.CUSTOM_EVENT,
|
|
144
|
+
},
|
|
145
|
+
eventId: generateId() as ActivityEventId,
|
|
146
|
+
isRead: false,
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
internalDoc.presenceManager.broadcastActivity(event);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
channel.postMessage({
|
|
153
|
+
type: "update",
|
|
154
|
+
data: Base64.fromUint8Array(update),
|
|
155
|
+
});
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
internalDoc.yDoc.on("update", updateHandler);
|
|
159
|
+
internalDoc.updateHandler = updateHandler;
|
|
160
|
+
|
|
161
|
+
channel.onmessage = event => {
|
|
162
|
+
if (event.data.type === "update") {
|
|
163
|
+
const update = Base64.toUint8Array(event.data.data);
|
|
164
|
+
Y.applyUpdate(internalDoc.yDoc, update, "remote");
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
get hasMetadataSubscriptions(): boolean {
|
|
170
|
+
return Array.from(this.documents.values()).some(
|
|
171
|
+
doc => this.hasSubscriptions(doc) && doc.metadataSubscribers.size > 0,
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
get hasStateSubscriptions(): boolean {
|
|
176
|
+
return Array.from(this.documents.values()).some(
|
|
177
|
+
doc => this.hasSubscriptions(doc) && doc.docStateSubscribers.size > 0,
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
readonly createDocument = async <T extends DocumentSchema>(
|
|
182
|
+
{ documentTypeName, name, security = EMPTY_DOCUMENT_SECURITY }: CreateDocumentMetadata,
|
|
183
|
+
schema: T,
|
|
184
|
+
): Promise<DocumentRef<T>> => {
|
|
185
|
+
await this.metadataStore.whenReady();
|
|
186
|
+
const ontologyRid = await getOntologyRid(this.app);
|
|
187
|
+
|
|
188
|
+
const id = generateDocumentId();
|
|
189
|
+
const docRef = createDocRef(this.app, id, schema);
|
|
190
|
+
|
|
191
|
+
const metadata: DocumentMetadata = {
|
|
192
|
+
documentTypeName,
|
|
193
|
+
name,
|
|
194
|
+
ontologyRid,
|
|
195
|
+
security, // TODO: may want to add in auth.getUserId() as owner here
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
this.metadataStore.addDocument(id, metadata);
|
|
199
|
+
|
|
200
|
+
const yDoc = this.initializeYDoc(schema);
|
|
201
|
+
this.getCreateInternalDoc(docRef, metadata, yDoc);
|
|
202
|
+
|
|
203
|
+
return docRef;
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
readonly searchDocuments = async <T extends DocumentSchema>(
|
|
207
|
+
documentTypeName: string,
|
|
208
|
+
schema: T,
|
|
209
|
+
options?: {
|
|
210
|
+
documentName?: string;
|
|
211
|
+
pageSize?: number;
|
|
212
|
+
pageToken?: string;
|
|
213
|
+
},
|
|
214
|
+
): Promise<SearchDocumentsResult> => {
|
|
215
|
+
await this.metadataStore.whenReady();
|
|
216
|
+
return this.metadataStore.searchDocuments(documentTypeName, options);
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
protected onMetadataSubscriptionOpened(
|
|
220
|
+
internalDoc: DemoInternalDoc,
|
|
221
|
+
docRef: DocumentRef,
|
|
222
|
+
): void {
|
|
223
|
+
this.updateMetadataStatus(internalDoc, docRef, {
|
|
224
|
+
load: DocumentLoadStatus.LOADING,
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
this.metadataStore.whenReady().then(() => {
|
|
228
|
+
const metadata = this.metadataStore.getDocument(docRef.id);
|
|
229
|
+
|
|
230
|
+
if (metadata == null) {
|
|
231
|
+
this.updateMetadataStatus(internalDoc, docRef, {
|
|
232
|
+
error: new Error("Document not found"),
|
|
233
|
+
load: DocumentLoadStatus.ERROR,
|
|
234
|
+
});
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
internalDoc.metadata = metadata;
|
|
239
|
+
|
|
240
|
+
const unobserve = this.metadataStore.observeDocument(docRef.id, updatedMetadata => {
|
|
241
|
+
if (updatedMetadata != null) {
|
|
242
|
+
internalDoc.metadata = updatedMetadata;
|
|
243
|
+
this.notifyMetadataSubscribers(internalDoc, docRef, updatedMetadata);
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
this.updateMetadataStatus(internalDoc, docRef, {
|
|
248
|
+
load: DocumentLoadStatus.LOADED,
|
|
249
|
+
});
|
|
250
|
+
}).catch((error: unknown) => {
|
|
251
|
+
this.updateMetadataStatus(internalDoc, docRef, {
|
|
252
|
+
error,
|
|
253
|
+
load: DocumentLoadStatus.ERROR,
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
protected onDataSubscriptionOpened(
|
|
259
|
+
internalDoc: DemoInternalDoc,
|
|
260
|
+
docRef: DocumentRef,
|
|
261
|
+
): void {
|
|
262
|
+
this.updateDataStatus(internalDoc, docRef, {
|
|
263
|
+
load: DocumentLoadStatus.LOADING,
|
|
264
|
+
live: DocumentLiveStatus.CONNECTING,
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
try {
|
|
268
|
+
// Ensure update handler is set up (may already be done in createInternalDoc)
|
|
269
|
+
this.ensureUpdateHandler(internalDoc, docRef);
|
|
270
|
+
|
|
271
|
+
// Set up persistence
|
|
272
|
+
if (!internalDoc.provider) {
|
|
273
|
+
const provider = new IndexeddbPersistence(
|
|
274
|
+
`${this.dbPrefix}-doc-${docRef.id}`,
|
|
275
|
+
internalDoc.yDoc,
|
|
276
|
+
);
|
|
277
|
+
internalDoc.provider = provider;
|
|
278
|
+
|
|
279
|
+
provider.whenSynced.then(() => {
|
|
280
|
+
this.updateDataStatus(internalDoc, docRef, {
|
|
281
|
+
load: DocumentLoadStatus.LOADED,
|
|
282
|
+
live: DocumentLiveStatus.CONNECTED,
|
|
283
|
+
});
|
|
284
|
+
}).catch((error: unknown) => {
|
|
285
|
+
this.updateDataStatus(internalDoc, docRef, {
|
|
286
|
+
error,
|
|
287
|
+
load: DocumentLoadStatus.ERROR,
|
|
288
|
+
live: DocumentLiveStatus.ERROR,
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
} else {
|
|
292
|
+
// Provider already exists, just update status
|
|
293
|
+
this.updateDataStatus(internalDoc, docRef, {
|
|
294
|
+
load: DocumentLoadStatus.LOADED,
|
|
295
|
+
live: DocumentLiveStatus.CONNECTED,
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
} catch (error) {
|
|
299
|
+
this.updateDataStatus(internalDoc, docRef, {
|
|
300
|
+
error,
|
|
301
|
+
load: DocumentLoadStatus.ERROR,
|
|
302
|
+
live: DocumentLiveStatus.ERROR,
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
protected onMetadataSubscriptionClosed(
|
|
308
|
+
_internalDoc: DemoInternalDoc,
|
|
309
|
+
_docRef: DocumentRef,
|
|
310
|
+
): void {
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
protected onDataSubscriptionClosed(
|
|
314
|
+
internalDoc: DemoInternalDoc,
|
|
315
|
+
_docRef: DocumentRef,
|
|
316
|
+
): void {
|
|
317
|
+
if (internalDoc.updateHandler) {
|
|
318
|
+
internalDoc.yDoc.off("update", internalDoc.updateHandler);
|
|
319
|
+
internalDoc.updateHandler = undefined;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (internalDoc.channel) {
|
|
323
|
+
internalDoc.channel.close();
|
|
324
|
+
internalDoc.channel = undefined;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (internalDoc.provider) {
|
|
328
|
+
void internalDoc.provider.destroy();
|
|
329
|
+
internalDoc.provider = undefined;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (internalDoc.presenceManager) {
|
|
333
|
+
internalDoc.presenceManager.dispose();
|
|
334
|
+
internalDoc.presenceManager = undefined;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
onActivity<T extends DocumentSchema>(
|
|
339
|
+
docRef: DocumentRef<T>,
|
|
340
|
+
callback: (docRef: DocumentRef<T>, event: ActivityEvent) => void,
|
|
341
|
+
): Unsubscribe {
|
|
342
|
+
const { internalDoc } = this.getCreateInternalDoc(docRef);
|
|
343
|
+
|
|
344
|
+
if (!internalDoc.presenceManager) {
|
|
345
|
+
internalDoc.presenceManager = new PresenceManager(docRef.id, this.clientId, docRef.schema);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const unsubscribe = internalDoc.presenceManager.onActivity(event => {
|
|
349
|
+
callback(docRef, event);
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
return () => {
|
|
353
|
+
unsubscribe();
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
onPresence<T extends DocumentSchema>(
|
|
358
|
+
docRef: DocumentRef<T>,
|
|
359
|
+
callback: (docRef: DocumentRef<T>, event: PresenceEvent) => void,
|
|
360
|
+
): Unsubscribe {
|
|
361
|
+
const { internalDoc } = this.getCreateInternalDoc(docRef);
|
|
362
|
+
|
|
363
|
+
if (!internalDoc.presenceManager) {
|
|
364
|
+
internalDoc.presenceManager = new PresenceManager(docRef.id, this.clientId, docRef.schema);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const unsubscribe = internalDoc.presenceManager.onPresence(event => {
|
|
368
|
+
callback(docRef, event);
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
return () => {
|
|
372
|
+
unsubscribe();
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
updateCustomPresence<M extends Model>(
|
|
377
|
+
docRef: DocumentRef,
|
|
378
|
+
model: M,
|
|
379
|
+
eventData: ModelData<M>,
|
|
380
|
+
): void {
|
|
381
|
+
const { internalDoc } = this.getCreateInternalDoc(docRef);
|
|
382
|
+
|
|
383
|
+
if (!internalDoc.presenceManager) {
|
|
384
|
+
internalDoc.presenceManager = new PresenceManager(docRef.id, this.clientId, docRef.schema);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const event: PresenceEvent = {
|
|
388
|
+
eventData: {
|
|
389
|
+
eventData,
|
|
390
|
+
model,
|
|
391
|
+
type: ActivityEventDataType.CUSTOM_EVENT,
|
|
392
|
+
},
|
|
393
|
+
userId: this.clientId as UserId,
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
internalDoc.presenceManager.broadcastPresence(event);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function generateDocumentId(): DocumentId {
|
|
401
|
+
return generateId() as DocumentId;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function isEditDescription(obj: unknown): obj is EditDescription {
|
|
405
|
+
return (
|
|
406
|
+
obj != null
|
|
407
|
+
&& typeof obj === "object"
|
|
408
|
+
&& "data" in obj
|
|
409
|
+
&& "model" in obj
|
|
410
|
+
&& typeof obj.model === "object"
|
|
411
|
+
&& obj.model != null
|
|
412
|
+
&& hasMetadata(obj.model)
|
|
413
|
+
);
|
|
414
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2025 Palantir Technologies, Inc. All rights reserved.
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { DocumentId, DocumentMetadata } from "@palantir/pack.document-schema.model-types";
|
|
18
|
+
import type { SearchDocumentsResult } from "@palantir/pack.state.core";
|
|
19
|
+
import { IndexeddbPersistence } from "y-indexeddb";
|
|
20
|
+
import * as Y from "yjs";
|
|
21
|
+
|
|
22
|
+
const METADATA_DB_NAME = "pack-demo-metadata";
|
|
23
|
+
const METADATA_MAP_KEY = "documents";
|
|
24
|
+
|
|
25
|
+
export class MetadataStore {
|
|
26
|
+
private readonly yDoc: Y.Doc;
|
|
27
|
+
private readonly persistence: IndexeddbPersistence;
|
|
28
|
+
private readonly metadataMap: Y.Map<DocumentMetadata>;
|
|
29
|
+
private isReady: boolean = false;
|
|
30
|
+
private readyPromise: Promise<void>;
|
|
31
|
+
|
|
32
|
+
constructor(dbPrefix: string = "pack-demo") {
|
|
33
|
+
this.yDoc = new Y.Doc();
|
|
34
|
+
this.metadataMap = this.yDoc.getMap<DocumentMetadata>(METADATA_MAP_KEY);
|
|
35
|
+
this.persistence = new IndexeddbPersistence(`${dbPrefix}-${METADATA_DB_NAME}`, this.yDoc);
|
|
36
|
+
|
|
37
|
+
this.readyPromise = this.persistence.whenSynced.then(() => {
|
|
38
|
+
this.isReady = true;
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async whenReady(): Promise<void> {
|
|
43
|
+
return this.readyPromise;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
addDocument(id: DocumentId, metadata: DocumentMetadata): void {
|
|
47
|
+
this.metadataMap.set(id, metadata);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
getDocument(id: DocumentId): DocumentMetadata | undefined {
|
|
51
|
+
return this.metadataMap.get(id);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
searchDocuments(
|
|
55
|
+
typeName: string,
|
|
56
|
+
options?: { documentName?: string; pageSize?: number; pageToken?: string },
|
|
57
|
+
): SearchDocumentsResult {
|
|
58
|
+
const allResults: Array<DocumentMetadata & { readonly id: DocumentId }> = [];
|
|
59
|
+
|
|
60
|
+
for (const [id, metadata] of this.metadataMap.entries()) {
|
|
61
|
+
if (metadata.documentTypeName !== typeName) {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (options?.documentName && metadata.name !== options.documentName) {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
allResults.push({ ...metadata, id: id as DocumentId });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const pageSize = options?.pageSize ?? allResults.length;
|
|
73
|
+
const offset = options?.pageToken != null ? parseInt(options.pageToken, 10) : 0;
|
|
74
|
+
const paginatedResults = allResults.slice(offset, offset + pageSize);
|
|
75
|
+
const hasMore = offset + pageSize < allResults.length;
|
|
76
|
+
const nextPageToken = hasMore ? String(offset + pageSize) : undefined;
|
|
77
|
+
|
|
78
|
+
return { data: paginatedResults, nextPageToken };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
observeDocument(
|
|
82
|
+
id: DocumentId,
|
|
83
|
+
callback: (metadata: DocumentMetadata | undefined) => void,
|
|
84
|
+
): () => void {
|
|
85
|
+
const handler = () => {
|
|
86
|
+
callback(this.metadataMap.get(id));
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
this.metadataMap.observe(handler);
|
|
90
|
+
|
|
91
|
+
return () => {
|
|
92
|
+
this.metadataMap.unobserve(handler);
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
dispose(): void {
|
|
97
|
+
void this.persistence.destroy();
|
|
98
|
+
this.yDoc.destroy();
|
|
99
|
+
}
|
|
100
|
+
}
|