@palantir/pack.state.foundry 0.0.1-beta.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.
@@ -0,0 +1,216 @@
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 { CreateDocumentRequest, DocumentSecurity } from "@osdk/foundry.pack";
18
+ import { Documents } from "@osdk/foundry.pack";
19
+ import type { ModuleConfigTuple, PackAppInternal } from "@palantir/pack.core";
20
+ import type {
21
+ DocumentId,
22
+ DocumentMetadata,
23
+ DocumentRef,
24
+ DocumentSchema,
25
+ } from "@palantir/pack.document-schema.model-types";
26
+ import type { DocumentService, InternalYjsDoc } from "@palantir/pack.state.core";
27
+ import {
28
+ BaseYjsDocumentService,
29
+ createDocumentServiceConfig,
30
+ DocumentLoadStatus,
31
+ } from "@palantir/pack.state.core";
32
+ import type { FoundryEventService, SyncSession } from "@palantir/pack.state.foundry-event";
33
+ import { createFoundryEventService } from "@palantir/pack.state.foundry-event";
34
+ import type * as y from "yjs";
35
+
36
+ const DEFAULT_USE_PREVIEW_API = true;
37
+
38
+ interface FoundryDocumentServiceConfig {
39
+ readonly usePreviewApi?: boolean;
40
+ }
41
+
42
+ export function createFoundryDocumentServiceConfig(
43
+ config: FoundryDocumentServiceConfig = {},
44
+ ): ModuleConfigTuple<DocumentService> {
45
+ return createDocumentServiceConfig(
46
+ (app: PackAppInternal, config: FoundryDocumentServiceConfig) =>
47
+ internalCreateFoundryDocumentService(app, config),
48
+ config,
49
+ );
50
+ }
51
+
52
+ export function internalCreateFoundryDocumentService(
53
+ app: PackAppInternal,
54
+ config: FoundryDocumentServiceConfig,
55
+ eventService?: FoundryEventService,
56
+ ): FoundryDocumentService {
57
+ return new FoundryDocumentService(
58
+ app,
59
+ config,
60
+ eventService ?? createFoundryEventService(app),
61
+ );
62
+ }
63
+
64
+ interface FoundryInternalDoc extends InternalYjsDoc {
65
+ syncSession?: SyncSession;
66
+ }
67
+
68
+ export class FoundryDocumentService extends BaseYjsDocumentService<FoundryInternalDoc> {
69
+ constructor(
70
+ app: PackAppInternal,
71
+ readonly config: FoundryDocumentServiceConfig,
72
+ readonly eventService: FoundryEventService,
73
+ ) {
74
+ super(
75
+ app,
76
+ app.config.logger.child({}, { level: "debug", msgPrefix: "FoundryDocumentService" }),
77
+ );
78
+ }
79
+
80
+ protected createInternalDoc(
81
+ ref: DocumentRef,
82
+ metadata: DocumentMetadata | undefined,
83
+ initialYDoc?: y.Doc,
84
+ ): FoundryInternalDoc {
85
+ return {
86
+ ...this.createBaseInternalDoc(ref, metadata, initialYDoc),
87
+ syncSession: undefined,
88
+ };
89
+ }
90
+
91
+ get hasMetadataSubscriptions(): boolean {
92
+ return Array.from(this.documents.values()).some(doc =>
93
+ this.hasSubscriptions(doc) && doc.metadataSubscribers.size > 0
94
+ );
95
+ }
96
+
97
+ get hasStateSubscriptions(): boolean {
98
+ return Array.from(this.documents.values()).some(doc =>
99
+ this.hasSubscriptions(doc) && doc.docStateSubscribers.size > 0
100
+ );
101
+ }
102
+
103
+ readonly createDocument = async <T extends DocumentSchema>(
104
+ metadata: DocumentMetadata,
105
+ schema: T,
106
+ ): Promise<DocumentRef<T>> => {
107
+ const { documentTypeName, name, ontologyRid, security } = metadata;
108
+
109
+ const request: CreateDocumentRequest = {
110
+ documentTypeName: documentTypeName,
111
+ name: name,
112
+ ontologyRid: ontologyRid,
113
+ security: getWireSecurity(security),
114
+ };
115
+ const createResponse = await Documents.create(this.app.config.osdkClient, request, {
116
+ preview: this.config.usePreviewApi ?? DEFAULT_USE_PREVIEW_API,
117
+ });
118
+
119
+ const documentId = createResponse.id as DocumentId;
120
+ const docRef = this.createDocRef(documentId, schema);
121
+ return docRef;
122
+ };
123
+
124
+ protected onMetadataSubscriptionOpened(
125
+ internalDoc: FoundryInternalDoc,
126
+ docRef: DocumentRef,
127
+ ): void {
128
+ if (internalDoc.metadataStatus.load !== DocumentLoadStatus.UNLOADED) {
129
+ throw new Error(
130
+ `Cannot subscribe to document metadata when status is ${internalDoc.metadataStatus.load}`,
131
+ );
132
+ }
133
+
134
+ this.updateMetadataStatus(internalDoc, docRef, {
135
+ load: DocumentLoadStatus.LOADING,
136
+ });
137
+
138
+ Documents.get(this.app.config.osdkClient, docRef.id, {
139
+ preview: this.config.usePreviewApi ?? DEFAULT_USE_PREVIEW_API,
140
+ })
141
+ .then(document => {
142
+ const metadata: DocumentMetadata = {
143
+ documentTypeName: document.documentTypeName,
144
+ name: document.name,
145
+ ontologyRid: "unknown",
146
+ security: { discretionary: { owners: [] }, mandatory: {} },
147
+ };
148
+
149
+ internalDoc.metadata = metadata;
150
+ this.notifyMetadataSubscribers(internalDoc, docRef, metadata);
151
+ this.updateMetadataStatus(internalDoc, docRef, {
152
+ load: DocumentLoadStatus.LOADED,
153
+ });
154
+ })
155
+ .catch((e: unknown) => {
156
+ const error = new Error("Failed to load document metadata", { cause: e });
157
+ this.updateMetadataStatus(internalDoc, docRef, {
158
+ error,
159
+ load: DocumentLoadStatus.ERROR,
160
+ });
161
+ });
162
+ }
163
+
164
+ protected onDataSubscriptionOpened(
165
+ internalDoc: FoundryInternalDoc,
166
+ docRef: DocumentRef,
167
+ ): void {
168
+ if (internalDoc.syncSession != null) {
169
+ throw new Error("Document data subscription already opened");
170
+ }
171
+
172
+ internalDoc.syncSession = this.eventService.startDocumentSync(
173
+ docRef.id,
174
+ internalDoc.yDoc,
175
+ status => {
176
+ this.updateDataStatus(internalDoc, docRef, status);
177
+ },
178
+ );
179
+ }
180
+
181
+ protected onMetadataSubscriptionClosed(
182
+ _internalDoc: FoundryInternalDoc,
183
+ _docRef: DocumentRef,
184
+ ): void {
185
+ }
186
+
187
+ protected onDataSubscriptionClosed(
188
+ internalDoc: FoundryInternalDoc,
189
+ _docRef: DocumentRef,
190
+ ): void {
191
+ if (internalDoc.syncSession) {
192
+ this.eventService.stopDocumentSync(internalDoc.syncSession);
193
+ internalDoc.syncSession = undefined;
194
+ }
195
+ }
196
+ }
197
+
198
+ function mutableArray<T>(array?: readonly T[]): T[] {
199
+ return array == null ? [] : (array as T[]);
200
+ }
201
+
202
+ function getWireSecurity(
203
+ security: DocumentMetadata["security"],
204
+ ): DocumentSecurity {
205
+ return {
206
+ discretionary: {
207
+ editors: [...(security.discretionary.editors ?? [])],
208
+ owners: [...security.discretionary.owners],
209
+ viewers: [...(security.discretionary.viewers ?? [])],
210
+ },
211
+ mandatory: {
212
+ classification: mutableArray(security.mandatory.classification),
213
+ markings: mutableArray(security.mandatory.markings),
214
+ },
215
+ };
216
+ }
@@ -0,0 +1,415 @@
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 { Document } from "@osdk/foundry.pack";
18
+ import { Documents } from "@osdk/foundry.pack";
19
+ import type { PackAppInternal } from "@palantir/pack.core";
20
+ import type { DocumentId, DocumentSchema } from "@palantir/pack.document-schema.model-types";
21
+ import { Metadata } from "@palantir/pack.document-schema.model-types";
22
+ import { createDocRef, DocumentLoadStatus, type DocumentStatus } from "@palantir/pack.state.core";
23
+ import type { FoundryEventService, SyncSession } from "@palantir/pack.state.foundry-event";
24
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
25
+ import type { MockProxy } from "vitest-mock-extended";
26
+ import { mock } from "vitest-mock-extended";
27
+ import type { FoundryDocumentService } from "../FoundryDocumentService.js";
28
+ import { internalCreateFoundryDocumentService } from "../FoundryDocumentService.js";
29
+
30
+ /* eslint-disable @typescript-eslint/unbound-method */
31
+
32
+ vi.mock("@osdk/foundry.pack", () => ({
33
+ Documents: {
34
+ get: vi.fn(),
35
+ },
36
+ }));
37
+
38
+ const mockAuthModule = {
39
+ onTokenChange: vi.fn(),
40
+ getToken: vi.fn().mockReturnValue("mock-token"),
41
+ };
42
+
43
+ const mockLogger = {
44
+ child: vi.fn(),
45
+ debug: vi.fn((...args: unknown[]) => {
46
+ console.log("[DEBUG]", ...args);
47
+ }),
48
+ error: vi.fn((...args: unknown[]) => {
49
+ console.error("[ERROR]", ...args);
50
+ }),
51
+ info: vi.fn((...args: unknown[]) => {
52
+ console.log("[INFO]", ...args);
53
+ }),
54
+ warn: vi.fn((...args: unknown[]) => {
55
+ console.warn("[WARN]", ...args);
56
+ }),
57
+ };
58
+
59
+ mockLogger.child.mockReturnValue(mockLogger);
60
+
61
+ const mockOsdkClient = {};
62
+
63
+ const mockApp = {
64
+ getModule: vi.fn().mockReturnValue(mockAuthModule),
65
+ config: {
66
+ logger: mockLogger,
67
+ osdkClient: mockOsdkClient,
68
+ remote: {
69
+ packWsPath: "/ws",
70
+ baseUrl: "https://test.example.com",
71
+ },
72
+ },
73
+ } as unknown as PackAppInternal;
74
+
75
+ const testSchema = {
76
+ [Metadata]: {
77
+ version: 1,
78
+ },
79
+ } as const satisfies DocumentSchema;
80
+
81
+ const mockDocument: Document = {
82
+ id: "test-doc",
83
+ name: "Test Document",
84
+ documentTypeName: "TestType",
85
+ } as Document;
86
+
87
+ describe("Foundry Document Status Tracking", () => {
88
+ let mockEventService: MockProxy<FoundryEventService>;
89
+ let service: FoundryDocumentService;
90
+ let sessionCounter: number;
91
+
92
+ beforeEach(() => {
93
+ vi.useFakeTimers();
94
+ mockEventService = mock();
95
+ sessionCounter = 0;
96
+
97
+ vi.mocked(Documents.get).mockResolvedValue(mockDocument);
98
+
99
+ mockEventService.startDocumentSync.mockImplementation((documentId, _yDoc, onStatusChange) => {
100
+ const session: SyncSession = {
101
+ clientId: `test-client-${++sessionCounter}`,
102
+ documentId,
103
+ };
104
+
105
+ void Promise.resolve().then(() => {
106
+ onStatusChange({
107
+ load: DocumentLoadStatus.LOADED,
108
+ });
109
+ });
110
+
111
+ return session;
112
+ });
113
+
114
+ mockEventService.stopDocumentSync.mockImplementation(() => {});
115
+
116
+ service = internalCreateFoundryDocumentService(mockApp, {}, mockEventService);
117
+ });
118
+
119
+ afterEach(() => {
120
+ vi.useRealTimers();
121
+ vi.mocked(Documents.get).mockClear();
122
+ });
123
+
124
+ describe("metadata loading", () => {
125
+ const unsubscribes: Array<() => void> = [];
126
+
127
+ afterEach(() => {
128
+ unsubscribes.forEach(fn => {
129
+ fn();
130
+ });
131
+ unsubscribes.length = 0;
132
+ });
133
+
134
+ it("should load metadata from backend on metadata subscription", async () => {
135
+ const docRef = createDocRef(mockApp, "test-doc-1" as DocumentId, testSchema);
136
+
137
+ const statusUpdates: DocumentStatus[] = [];
138
+
139
+ unsubscribes.push(service.onStatusChange(docRef, (_, status) => {
140
+ statusUpdates.push(status);
141
+ }));
142
+
143
+ unsubscribes.push(service.onMetadataChange(docRef, () => {}));
144
+
145
+ await vi.runAllTimersAsync();
146
+
147
+ expect(Documents.get).toHaveBeenCalledWith(mockOsdkClient, "test-doc-1", {
148
+ preview: true,
149
+ });
150
+
151
+ const finalStatus = statusUpdates.at(-1);
152
+ expect(finalStatus).toBeDefined();
153
+ expect(finalStatus?.metadata.load).toBe(DocumentLoadStatus.LOADED);
154
+ expect(finalStatus?.metadataError).toBeUndefined();
155
+ });
156
+
157
+ it("should handle backend loading errors", async () => {
158
+ const error = new Error("Backend failed");
159
+ vi.mocked(Documents.get).mockRejectedValueOnce(error);
160
+
161
+ const docRef = createDocRef(mockApp, "test-doc-2" as DocumentId, testSchema);
162
+
163
+ const statusUpdates: DocumentStatus[] = [];
164
+
165
+ unsubscribes.push(service.onStatusChange(docRef, (_, status) => {
166
+ statusUpdates.push(status);
167
+ }));
168
+
169
+ unsubscribes.push(service.onMetadataChange(docRef, () => {}));
170
+
171
+ await vi.runAllTimersAsync();
172
+
173
+ const finalStatus = statusUpdates.at(-1);
174
+ expect(finalStatus).toBeDefined();
175
+ expect(finalStatus?.metadata.load).toBe(DocumentLoadStatus.ERROR);
176
+ expect(finalStatus?.metadataError).toMatchObject({
177
+ cause: error,
178
+ message: "Failed to load document metadata",
179
+ });
180
+ });
181
+
182
+ it("should not reload from backend if already loaded", async () => {
183
+ const docRef = createDocRef(mockApp, "test-doc-3" as DocumentId, testSchema);
184
+
185
+ unsubscribes.push(service.onMetadataChange(docRef, () => {}));
186
+ await vi.runAllTimersAsync();
187
+
188
+ vi.mocked(Documents.get).mockClear();
189
+
190
+ unsubscribes.push(service.onMetadataChange(docRef, () => {}));
191
+ await vi.runAllTimersAsync();
192
+
193
+ expect(Documents.get).not.toHaveBeenCalled();
194
+ });
195
+
196
+ it("should handle concurrent subscriptions efficiently", async () => {
197
+ const docRef = createDocRef(mockApp, "test-doc-4" as DocumentId, testSchema);
198
+
199
+ unsubscribes.push(
200
+ service.onMetadataChange(docRef, () => {}),
201
+ service.onMetadataChange(docRef, () => {}),
202
+ service.onMetadataChange(docRef, () => {}),
203
+ );
204
+
205
+ await vi.runAllTimersAsync();
206
+
207
+ expect(Documents.get).toHaveBeenCalledTimes(1);
208
+ });
209
+
210
+ it("should reject waitForLoad promises on error", async () => {
211
+ const error = new Error("Test failure!");
212
+ vi.mocked(Documents.get).mockRejectedValueOnce(error);
213
+
214
+ const docRef = createDocRef(mockApp, "test-doc-5" as DocumentId, testSchema);
215
+
216
+ unsubscribes.push(service.onMetadataChange(docRef, () => {}));
217
+
218
+ await expect(service.waitForMetadataLoad(docRef)).rejects.toThrow("Metadata load error");
219
+ });
220
+
221
+ it("should resolve waitForLoad promises when already loaded", async () => {
222
+ const docRef = createDocRef(mockApp, "test-doc-6" as DocumentId, testSchema);
223
+
224
+ unsubscribes.push(service.onMetadataChange(docRef, () => {}));
225
+ await vi.runAllTimersAsync();
226
+
227
+ await expect(service.waitForMetadataLoad(docRef)).resolves.toBeUndefined();
228
+ });
229
+ });
230
+
231
+ describe("websocket data loading", () => {
232
+ const unsubscribes: Array<() => void> = [];
233
+
234
+ afterEach(() => {
235
+ unsubscribes.forEach(fn => {
236
+ fn();
237
+ });
238
+ unsubscribes.length = 0;
239
+ });
240
+
241
+ it("should load data via websocket on data subscription", async () => {
242
+ const docRef = createDocRef(mockApp, "test-doc-7" as DocumentId, testSchema);
243
+
244
+ const statusUpdates: DocumentStatus[] = [];
245
+
246
+ unsubscribes.push(service.onStatusChange(docRef, (_, status) => {
247
+ statusUpdates.push(status);
248
+ }));
249
+
250
+ unsubscribes.push(service.onStateChange(docRef, () => {}));
251
+
252
+ await vi.runAllTimersAsync();
253
+
254
+ expect(Documents.get).not.toHaveBeenCalled();
255
+ expect(mockEventService.startDocumentSync).toHaveBeenCalled();
256
+
257
+ const finalStatus = statusUpdates.at(-1);
258
+ expect(finalStatus).toBeDefined();
259
+ expect(finalStatus?.data.load).toBe(DocumentLoadStatus.LOADED);
260
+ expect(finalStatus?.dataError).toBeUndefined();
261
+ });
262
+
263
+ it("should handle fast unsubscribe before websocket subscription completes", async () => {
264
+ const docRef = createDocRef(mockApp, "test-doc-8" as DocumentId, testSchema);
265
+
266
+ const statusUpdates: DocumentStatus[] = [];
267
+ unsubscribes.push(service.onStatusChange(docRef, (_, status) => {
268
+ statusUpdates.push(status);
269
+ }));
270
+
271
+ const unsubscribeState = service.onStateChange(docRef, () => {});
272
+
273
+ unsubscribeState();
274
+
275
+ await vi.runAllTimersAsync();
276
+
277
+ expect(mockEventService.stopDocumentSync).toHaveBeenCalled();
278
+ });
279
+
280
+ it("should handle websocket subscription errors and update data status to ERROR", async () => {
281
+ mockEventService.startDocumentSync.mockImplementationOnce(
282
+ (documentId, _yDoc, onStatusChange) => {
283
+ const session: SyncSession = {
284
+ clientId: `test-client-${++sessionCounter}`,
285
+ documentId,
286
+ };
287
+
288
+ void Promise.resolve().then(() => {
289
+ onStatusChange({
290
+ error: new Error("WebSocket subscription failed"),
291
+ load: DocumentLoadStatus.ERROR,
292
+ });
293
+ });
294
+
295
+ return session;
296
+ },
297
+ );
298
+
299
+ const docRef = createDocRef(mockApp, "test-doc-9" as DocumentId, testSchema);
300
+
301
+ const statusUpdates: DocumentStatus[] = [];
302
+ unsubscribes.push(service.onStatusChange(docRef, (_, status) => {
303
+ statusUpdates.push(status);
304
+ }));
305
+
306
+ unsubscribes.push(service.onStateChange(docRef, () => {}));
307
+
308
+ await vi.runAllTimersAsync();
309
+
310
+ const finalStatus = statusUpdates.at(-1);
311
+ expect(finalStatus).toBeDefined();
312
+ expect(finalStatus?.data.load).toBe(DocumentLoadStatus.ERROR);
313
+ expect(finalStatus?.dataError).toBeDefined();
314
+ });
315
+
316
+ it("should handle error messages from websocket and update data status", async () => {
317
+ mockEventService.startDocumentSync.mockImplementationOnce(
318
+ (documentId, _yDoc, onStatusChange) => {
319
+ const session: SyncSession = {
320
+ clientId: `test-client-${++sessionCounter}`,
321
+ documentId,
322
+ };
323
+
324
+ void Promise.resolve().then(() => {
325
+ onStatusChange({
326
+ load: DocumentLoadStatus.LOADED,
327
+ });
328
+ }).then(() => {
329
+ onStatusChange({
330
+ error: new Error("Sync failed"),
331
+ load: DocumentLoadStatus.ERROR,
332
+ });
333
+ });
334
+
335
+ return session;
336
+ },
337
+ );
338
+
339
+ const docRef = createDocRef(mockApp, "test-doc-10" as DocumentId, testSchema);
340
+
341
+ const statusUpdates: DocumentStatus[] = [];
342
+ unsubscribes.push(service.onStatusChange(docRef, (_, status) => {
343
+ statusUpdates.push(status);
344
+ }));
345
+
346
+ unsubscribes.push(service.onStateChange(docRef, () => {}));
347
+
348
+ await vi.runAllTimersAsync();
349
+
350
+ const finalStatus = statusUpdates.at(-1);
351
+ expect(finalStatus).toBeDefined();
352
+ expect(finalStatus?.data.load).toBe(DocumentLoadStatus.ERROR);
353
+ expect(finalStatus?.dataError).toBeDefined();
354
+ });
355
+
356
+ it("should update data status to LOADED after successful websocket subscription", async () => {
357
+ const docRef = createDocRef(mockApp, "test-doc-11" as DocumentId, testSchema);
358
+
359
+ const statusUpdates: DocumentStatus[] = [];
360
+ unsubscribes.push(service.onStatusChange(docRef, (_, status) => {
361
+ statusUpdates.push(status);
362
+ }));
363
+
364
+ unsubscribes.push(service.onStateChange(docRef, () => {}));
365
+
366
+ await vi.runAllTimersAsync();
367
+
368
+ const statusAfterSubscribe = statusUpdates.at(-1);
369
+ expect(statusAfterSubscribe).toBeDefined();
370
+ expect(statusAfterSubscribe?.data.load).toBe(DocumentLoadStatus.LOADED);
371
+ expect(statusAfterSubscribe?.dataError).toBeUndefined();
372
+ });
373
+ });
374
+
375
+ describe("combined metadata and data loading", () => {
376
+ const unsubscribes: Array<() => void> = [];
377
+
378
+ afterEach(() => {
379
+ unsubscribes.forEach(fn => {
380
+ fn();
381
+ });
382
+ unsubscribes.length = 0;
383
+ });
384
+
385
+ it("should separate metadata and data loading", async () => {
386
+ const docRef = createDocRef(mockApp, "test-doc-12" as DocumentId, testSchema);
387
+
388
+ const statusUpdatesBeforeData: DocumentStatus[] = [];
389
+ const statusUpdatesAfterData: DocumentStatus[] = [];
390
+
391
+ unsubscribes.push(service.onStatusChange(docRef, (_, status) => {
392
+ statusUpdatesAfterData.push(status);
393
+ }));
394
+
395
+ unsubscribes.push(service.onMetadataChange(docRef, () => {}));
396
+ await vi.runAllTimersAsync();
397
+
398
+ unsubscribes.push(service.onStatusChange(docRef, (_, status) => {
399
+ statusUpdatesBeforeData.push(status);
400
+ }));
401
+
402
+ unsubscribes.push(service.onStateChange(docRef, () => {}));
403
+
404
+ await vi.runAllTimersAsync();
405
+
406
+ expect(Documents.get).toHaveBeenCalledTimes(1);
407
+ expect(mockEventService.startDocumentSync).toHaveBeenCalled();
408
+
409
+ const finalStatus = statusUpdatesAfterData.at(-1);
410
+ expect(finalStatus).toBeDefined();
411
+ expect(finalStatus?.metadata.load).toBe(DocumentLoadStatus.LOADED);
412
+ expect(finalStatus?.data.load).toBe(DocumentLoadStatus.LOADED);
413
+ });
414
+ });
415
+ });
package/src/index.ts ADDED
@@ -0,0 +1,17 @@
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
+ export { createFoundryDocumentServiceConfig } from "./FoundryDocumentService.js";
package/tsconfig.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "extends": "@palantir/pack.monorepo.tsconfig/base.json",
3
+ "compilerOptions": {
4
+ "outDir": "./build/esm",
5
+ "rootDir": "./src",
6
+ "declarationDir": "./build/types",
7
+ "tsBuildInfoFile": "./build/tsconfig.tsbuildinfo"
8
+ },
9
+ "include": [
10
+ "src/**/*"
11
+ ],
12
+ "exclude": [
13
+ "build",
14
+ "dist",
15
+ "node_modules",
16
+ "test-output",
17
+ "**/__tests__/**/__snapshots__",
18
+ "**/__tests__/**/fixtures"
19
+ ],
20
+ "references": []
21
+ }
@@ -0,0 +1,26 @@
1
+ import { coverageConfigDefaults, defaultExclude, defineProject } from "vitest/config";
2
+
3
+ export default defineProject({
4
+ test: {
5
+ exclude: [...defaultExclude, "**/build/**", "**/test-output/**"],
6
+ coverage: {
7
+ provider: "v8",
8
+ all: true,
9
+ enabled: true,
10
+ pool: "forks",
11
+ include: ["src/**/*.{ts,tsx}"],
12
+ exclude: [
13
+ ...coverageConfigDefaults.exclude,
14
+ "**/*.test.{ts,tsx}",
15
+ "**/__tests__",
16
+ "build",
17
+ "coverage",
18
+ "dist",
19
+ "node_modules",
20
+ "src/index.ts",
21
+ "test-output",
22
+ "vitest.config.*",
23
+ ],
24
+ },
25
+ },
26
+ });