@palantir/pack.state.core 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.
Files changed (63) hide show
  1. package/.turbo/turbo-lint.log +4 -0
  2. package/.turbo/turbo-transpileBrowser.log +5 -0
  3. package/.turbo/turbo-transpileCjs.log +5 -0
  4. package/.turbo/turbo-transpileEsm.log +5 -0
  5. package/.turbo/turbo-transpileTypes.log +5 -0
  6. package/.turbo/turbo-typecheck.log +4 -0
  7. package/LICENSE.txt +13 -0
  8. package/README.md +55 -0
  9. package/build/browser/index.js +1257 -0
  10. package/build/browser/index.js.map +1 -0
  11. package/build/cjs/index.cjs +1298 -0
  12. package/build/cjs/index.cjs.map +1 -0
  13. package/build/cjs/index.d.cts +272 -0
  14. package/build/esm/index.js +1257 -0
  15. package/build/esm/index.js.map +1 -0
  16. package/build/types/DocumentServiceModule.d.ts +6 -0
  17. package/build/types/DocumentServiceModule.d.ts.map +1 -0
  18. package/build/types/__tests__/DocumentStatusTracking.test.d.ts +1 -0
  19. package/build/types/__tests__/DocumentStatusTracking.test.d.ts.map +1 -0
  20. package/build/types/__tests__/RefStability.test.d.ts +1 -0
  21. package/build/types/__tests__/RefStability.test.d.ts.map +1 -0
  22. package/build/types/__tests__/StateModule.integration.test.d.ts +1 -0
  23. package/build/types/__tests__/StateModule.integration.test.d.ts.map +1 -0
  24. package/build/types/__tests__/testUtils.d.ts +7 -0
  25. package/build/types/__tests__/testUtils.d.ts.map +1 -0
  26. package/build/types/index.d.ts +11 -0
  27. package/build/types/index.d.ts.map +1 -0
  28. package/build/types/service/BaseYjsDocumentService.d.ts +155 -0
  29. package/build/types/service/BaseYjsDocumentService.d.ts.map +1 -0
  30. package/build/types/service/InMemoryDocumentService.d.ts +12 -0
  31. package/build/types/service/InMemoryDocumentService.d.ts.map +1 -0
  32. package/build/types/service/YjsSchemaMapper.d.ts +9 -0
  33. package/build/types/service/YjsSchemaMapper.d.ts.map +1 -0
  34. package/build/types/types/DocumentRefImpl.d.ts +5 -0
  35. package/build/types/types/DocumentRefImpl.d.ts.map +1 -0
  36. package/build/types/types/DocumentService.d.ts +62 -0
  37. package/build/types/types/DocumentService.d.ts.map +1 -0
  38. package/build/types/types/DocumentServiceConfig.d.ts +5 -0
  39. package/build/types/types/DocumentServiceConfig.d.ts.map +1 -0
  40. package/build/types/types/RecordCollectionRefImpl.d.ts +5 -0
  41. package/build/types/types/RecordCollectionRefImpl.d.ts.map +1 -0
  42. package/build/types/types/RecordRefImpl.d.ts +5 -0
  43. package/build/types/types/RecordRefImpl.d.ts.map +1 -0
  44. package/build/types/types/StateModule.d.ts +59 -0
  45. package/build/types/types/StateModule.d.ts.map +1 -0
  46. package/package.json +71 -0
  47. package/src/DocumentServiceModule.ts +53 -0
  48. package/src/__tests__/DocumentStatusTracking.test.ts +229 -0
  49. package/src/__tests__/RefStability.test.ts +441 -0
  50. package/src/__tests__/StateModule.integration.test.ts +1187 -0
  51. package/src/__tests__/testUtils.ts +106 -0
  52. package/src/index.ts +38 -0
  53. package/src/service/BaseYjsDocumentService.ts +1277 -0
  54. package/src/service/InMemoryDocumentService.ts +162 -0
  55. package/src/service/YjsSchemaMapper.ts +194 -0
  56. package/src/types/DocumentRefImpl.ts +98 -0
  57. package/src/types/DocumentService.ts +210 -0
  58. package/src/types/DocumentServiceConfig.ts +22 -0
  59. package/src/types/RecordCollectionRefImpl.ts +124 -0
  60. package/src/types/RecordRefImpl.ts +106 -0
  61. package/src/types/StateModule.ts +329 -0
  62. package/tsconfig.json +21 -0
  63. package/vitest.config.mjs +26 -0
@@ -0,0 +1,5 @@
1
+ import type { DocumentRef, Model, RecordCollectionRef } from "@palantir/pack.document-schema.model-types";
2
+ import type { DocumentService } from "./DocumentService.js";
3
+ export declare const createRecordCollectionRef: <const M extends Model>(documentService: DocumentService, docRef: DocumentRef, model: M) => RecordCollectionRef<M>;
4
+ export declare function invalidRecordCollectionRef<M extends Model = Model>(): RecordCollectionRef<M>;
5
+ export declare function isValidRecordCollectionRef<M extends Model = Model>(collectionRef: RecordCollectionRef<M>): collectionRef is RecordCollectionRef<M>;
@@ -0,0 +1 @@
1
+ {"mappings":"AAgBA,cACE,aACA,OAEA,2BAIK;AAGP,cAAc,uBAAuB;AAqBrC,OAAO,cAAM,kCAAmC,UAAU,OACxD,iBAAiB,iBACjB,QAAQ,aACR,OAAO,MACN,oBAAoB;AAIvB,OAAO,iBAAS,2BAA2B,UAAU,QAAQ,UAAU,oBAAoB;AAI3F,OAAO,iBAAS,2BAA2B,UAAU,QAAQ,OAC3D,eAAe,oBAAoB,KAClC,iBAAiB,oBAAoB","names":[],"sources":["../../../src/types/RecordCollectionRefImpl.ts"],"version":3,"file":"RecordCollectionRefImpl.d.ts"}
@@ -0,0 +1,5 @@
1
+ import type { DocumentRef, Model, RecordId, RecordRef } from "@palantir/pack.document-schema.model-types";
2
+ import type { DocumentService } from "./DocumentService.js";
3
+ export declare const createRecordRef: <const M extends Model>(documentService: DocumentService, docRef: DocumentRef, id: RecordId, model: M) => RecordRef<M>;
4
+ export declare function invalidRecordRef<M extends Model = Model>(): RecordRef<M>;
5
+ export declare function isValidRecordRef<M extends Model = Model>(recordRef: RecordRef<M>): recordRef is RecordRef<M>;
@@ -0,0 +1 @@
1
+ {"mappings":"AAgBA,cACE,aACA,OAEA,UACA,iBAEK;AAGP,cAAc,uBAAuB;AAgBrC,OAAO,cAAM,wBAAyB,UAAU,OAC9C,iBAAiB,iBACjB,QAAQ,aACR,IAAI,UACJ,OAAO,MACN,UAAU;AAIb,OAAO,iBAAS,iBAAiB,UAAU,QAAQ,UAAU,UAAU;AAIvE,OAAO,iBAAS,iBAAiB,UAAU,QAAQ,OACjD,WAAW,UAAU,KACpB,aAAa,UAAU","names":[],"sources":["../../../src/types/RecordRefImpl.ts"],"version":3,"file":"RecordRefImpl.d.ts"}
@@ -0,0 +1,59 @@
1
+ import { type ModuleKey, type PackAppInternal, type Unsubscribe } from "@palantir/pack.core";
2
+ import type { DocumentId, DocumentMetadata, DocumentRef, DocumentSchema, DocumentState, Model, ModelData, RecordCollectionRef, RecordId, RecordRef } from "@palantir/pack.document-schema.model-types";
3
+ import type { DocumentService, RecordChangeCallback, RecordCollectionChangeCallback, RecordDeleteCallback } from "./DocumentService.js";
4
+ export declare const STATE_MODULE_ACCESSOR = "state";
5
+ export declare const STATE_MODULE_KEY: ModuleKey<StateModuleImpl>;
6
+ export type WithStateModule<T> = T & {
7
+ readonly [STATE_MODULE_ACCESSOR]: StateModule;
8
+ };
9
+ export interface StateModule {
10
+ readonly createDocRef: <const T extends DocumentSchema>(id: DocumentId, schema: T) => DocumentRef<T>;
11
+ readonly createRecordRef: <const M extends Model>(docRef: DocumentRef, id: RecordId, model: M) => RecordRef<M>;
12
+ readonly createDocument: <T extends DocumentSchema>(metadata: DocumentMetadata, schema: T) => Promise<DocumentRef<T>>;
13
+ readonly getDocumentSnapshot: <T extends DocumentSchema>(docRef: DocumentRef<T>) => Promise<DocumentState<T>>;
14
+ readonly onMetadataChange: <T extends DocumentSchema>(docRef: DocumentRef<T>, cb: (docRef: DocumentRef<T>, metadata: DocumentMetadata) => void) => Unsubscribe;
15
+ readonly onStateChange: <T extends DocumentSchema>(docRef: DocumentRef<T>, cb: (docRef: DocumentRef<T>) => void) => Unsubscribe;
16
+ readonly getRecordSnapshot: <R extends Model>(recordRef: RecordRef<R>) => Promise<ModelData<R>>;
17
+ readonly onCollectionItemsAdded: <M extends Model>(collection: RecordCollectionRef<M>, callback: RecordCollectionChangeCallback<M>) => Unsubscribe;
18
+ readonly onCollectionItemsChanged: <M extends Model>(collection: RecordCollectionRef<M>, callback: RecordCollectionChangeCallback<M>) => Unsubscribe;
19
+ readonly onCollectionItemsDeleted: <M extends Model>(collection: RecordCollectionRef<M>, callback: RecordCollectionChangeCallback<M>) => Unsubscribe;
20
+ readonly setRecord: <R extends Model>(recordRef: RecordRef<R>, state: ModelData<R>) => Promise<void>;
21
+ readonly updateRecord: <R extends Model>(recordRef: RecordRef<R>, partialState: Partial<ModelData<R>>) => Promise<void>;
22
+ readonly onRecordChanged: <M extends Model>(record: RecordRef<M>, callback: RecordChangeCallback<M>) => Unsubscribe;
23
+ readonly onRecordDeleted: <M extends Model>(record: RecordRef<M>, callback: RecordDeleteCallback<M>) => Unsubscribe;
24
+ readonly deleteRecord: <M extends Model>(record: RecordRef<M>) => Promise<void>;
25
+ readonly getDocumentStatus: <T extends DocumentSchema>(docRef: DocumentRef<T>) => ReturnType<DocumentService["getDocumentStatus"]>;
26
+ readonly onStatusChange: <T extends DocumentSchema>(docRef: DocumentRef<T>, callback: Parameters<DocumentService["onStatusChange"]>[1]) => Unsubscribe;
27
+ readonly waitForMetadataLoad: <T extends DocumentSchema>(docRef: DocumentRef<T>) => Promise<void>;
28
+ readonly waitForDataLoad: <T extends DocumentSchema>(docRef: DocumentRef<T>) => Promise<void>;
29
+ }
30
+ export declare class StateModuleImpl implements StateModule {
31
+ private readonly documentService;
32
+ constructor(documentService: DocumentService);
33
+ createDocRef<const T extends DocumentSchema>(id: DocumentId, schema: T): DocumentRef<T>;
34
+ createRecordRef<const M extends Model>(docRef: DocumentRef, id: RecordId, model: M): RecordRef<M>;
35
+ createDocument<T extends DocumentSchema>(metadata: DocumentMetadata, schema: T): Promise<DocumentRef<T>>;
36
+ getDocumentSnapshot<T extends DocumentSchema>(docRef: DocumentRef<T>): Promise<DocumentState<T>>;
37
+ onMetadataChange<T extends DocumentSchema>(docRef: DocumentRef<T>, cb: (doc: DocumentRef<T>, metadata: DocumentMetadata) => void): Unsubscribe;
38
+ onStateChange<T extends DocumentSchema>(docRef: DocumentRef<T>, cb: (docRef: DocumentRef<T>) => void): Unsubscribe;
39
+ getRecordSnapshot<R extends Model>(recordRef: RecordRef<R>): Promise<ModelData<R>>;
40
+ setRecord<R extends Model>(recordRef: RecordRef<R>, state: ModelData<R>): Promise<void>;
41
+ updateRecord<R extends Model>(recordRef: RecordRef<R>, partialState: Partial<ModelData<R>>): Promise<void>;
42
+ getCreateRecordCollectionRef<M extends Model>(docRef: DocumentRef, model: M): RecordCollectionRef<M>;
43
+ getRecord<M extends Model>(collection: RecordCollectionRef<M>, id: RecordId): RecordRef<M> | undefined;
44
+ hasRecord<M extends Model>(collection: RecordCollectionRef<M>, id: RecordId): boolean;
45
+ setCollectionRecord<M extends Model>(collection: RecordCollectionRef<M>, id: RecordId, state: ModelData<M>): Promise<void>;
46
+ getCollectionSize<M extends Model>(collection: RecordCollectionRef<M>): number;
47
+ getCollectionRecords<M extends Model>(collection: RecordCollectionRef<M>): RecordRef<M>[];
48
+ onRecordChanged<M extends Model>(record: RecordRef<M>, callback: RecordChangeCallback<M>): Unsubscribe;
49
+ onRecordDeleted<M extends Model>(record: RecordRef<M>, callback: RecordDeleteCallback<M>): Unsubscribe;
50
+ onCollectionItemsAdded<M extends Model>(collection: RecordCollectionRef<M>, callback: RecordCollectionChangeCallback<M>): Unsubscribe;
51
+ onCollectionItemsChanged<M extends Model>(collection: RecordCollectionRef<M>, callback: RecordCollectionChangeCallback<M>): Unsubscribe;
52
+ onCollectionItemsDeleted<M extends Model>(collection: RecordCollectionRef<M>, callback: RecordCollectionChangeCallback<M>): Unsubscribe;
53
+ getDocumentStatus<T extends DocumentSchema>(docRef: DocumentRef<T>): ReturnType<DocumentService["getDocumentStatus"]>;
54
+ onStatusChange<T extends DocumentSchema>(docRef: DocumentRef<T>, callback: Parameters<DocumentService["onStatusChange"]>[1]): Unsubscribe;
55
+ waitForMetadataLoad<T extends DocumentSchema>(docRef: DocumentRef<T>): Promise<void>;
56
+ waitForDataLoad<T extends DocumentSchema>(docRef: DocumentRef<T>): Promise<void>;
57
+ deleteRecord<M extends Model>(record: RecordRef<M>): Promise<void>;
58
+ }
59
+ export declare function getStateModule(app: PackAppInternal): StateModule;
@@ -0,0 +1 @@
1
+ {"mappings":"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","names":["STATE_MODULE_KEY: ModuleKey<StateModuleImpl>","documentService: DocumentService"],"sources":["../../../src/types/StateModule.ts"],"version":3,"file":"StateModule.d.ts"}
package/package.json ADDED
@@ -0,0 +1,71 @@
1
+ {
2
+ "name": "@palantir/pack.state.core",
3
+ "version": "0.0.1-beta.1",
4
+ "description": "PACK State logic and utilities",
5
+ "license": "Apache-2.0",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/palantir/pack.git"
9
+ },
10
+ "exports": {
11
+ ".": {
12
+ "browser": "./build/browser/index.js",
13
+ "import": {
14
+ "types": "./build/types/index.d.ts",
15
+ "default": "./build/esm/index.js"
16
+ },
17
+ "require": "./build/cjs/index.js",
18
+ "default": "./build/browser/index.js"
19
+ },
20
+ "./*": {
21
+ "browser": "./build/browser/public/*.js",
22
+ "import": {
23
+ "types": "./build/types/public/*.d.ts",
24
+ "default": "./build/js/public/*.js"
25
+ },
26
+ "require": "./build/cjs/public/*.js",
27
+ "default": "./build/browser/public/*.js"
28
+ }
29
+ },
30
+ "dependencies": {
31
+ "@osdk/api": "~2.4.2",
32
+ "remeda": "^2.32.0",
33
+ "tiny-invariant": "^1.3.3",
34
+ "yjs": "^13.6.27",
35
+ "@palantir/pack.document-schema.model-types": "0.1.0-beta.2",
36
+ "@palantir/pack.core": "0.0.1-beta.1"
37
+ },
38
+ "peerDependencies": {
39
+ "zod": "^4.1.0"
40
+ },
41
+ "devDependencies": {
42
+ "@osdk/client": "~2.4.2",
43
+ "rimraf": "^6.0.1",
44
+ "tslib": "^2.8.1",
45
+ "typescript": "^5.9.2",
46
+ "vitest-mock-extended": "^3.1.0",
47
+ "@palantir/pack.monorepo.tsconfig": "~0.4.0-beta.1"
48
+ },
49
+ "publishConfig": {
50
+ "access": "public"
51
+ },
52
+ "keywords": [
53
+ "pack"
54
+ ],
55
+ "main": "./build/cjs/index.js",
56
+ "module": "./build/esm/index.js",
57
+ "types": "./build/cjs/index.d.ts",
58
+ "type": "module",
59
+ "scripts": {
60
+ "clean": "rimraf .turbo build dist lib test-output *.tgz tsconfig.tsbuildinfo",
61
+ "lint": "eslint ./src ; dprint check --config $(find-up dprint.json) --allow-no-files",
62
+ "lint:fix": "eslint ./src --fix ; dprint fmt --config $(find-up dprint.json) --allow-no-files",
63
+ "test": "vitest run --passWithNoTests -u",
64
+ "test:watch": "vitest --passWithNoTests",
65
+ "transpileBrowser": "monorepo-transpile -f esm -m bundle -t browser",
66
+ "transpileCjs": "monorepo-transpile -f cjs -m bundle -t node",
67
+ "transpileEsm": "monorepo-transpile -f esm -m bundle -t node",
68
+ "transpileTypes": "monorepo-transpile -f esm -m types -t node",
69
+ "typecheck": "tsc --noEmit --emitDeclarationOnly false"
70
+ }
71
+ }
@@ -0,0 +1,53 @@
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 { ModuleConfigTuple, ModuleKey, PackAppInternal } from "@palantir/pack.core";
18
+ import type { DocumentService } from "./types/DocumentService.js";
19
+ import type { WithDocumentServiceInit } from "./types/DocumentServiceConfig.js";
20
+
21
+ export const DOCUMENT_SERVICE_MODULE_KEY: ModuleKey<
22
+ DocumentService,
23
+ WithDocumentServiceInit
24
+ > = {
25
+ key: Symbol("DocumentService"),
26
+ initModule: initDocumentService,
27
+ };
28
+
29
+ export function getDocumentService(app: PackAppInternal): DocumentService {
30
+ return app.getModule(DOCUMENT_SERVICE_MODULE_KEY);
31
+ }
32
+
33
+ export function createDocumentServiceConfig<T>(
34
+ init: (app: PackAppInternal, config: T) => DocumentService,
35
+ config: T,
36
+ ): ModuleConfigTuple<DocumentService> {
37
+ return [DOCUMENT_SERVICE_MODULE_KEY as ModuleKey<DocumentService>, {
38
+ ...config,
39
+ init,
40
+ } as WithDocumentServiceInit] as const;
41
+ }
42
+
43
+ function initDocumentService(
44
+ app: PackAppInternal,
45
+ config?: WithDocumentServiceInit,
46
+ ): DocumentService {
47
+ if (config == null) {
48
+ throw new Error(
49
+ "DocumentServiceConfig is required to initialize DocumentService",
50
+ );
51
+ }
52
+ return config.init(app, config);
53
+ }
@@ -0,0 +1,229 @@
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, DocumentSchema } from "@palantir/pack.document-schema.model-types";
18
+ import { Metadata } from "@palantir/pack.document-schema.model-types";
19
+ import { describe, expect, it } from "vitest";
20
+ import { internalCreateInMemoryDocumentService } from "../service/InMemoryDocumentService.js";
21
+ import { createDocRef } from "../types/DocumentRefImpl.js";
22
+ import type { DocumentStatus } from "../types/DocumentService.js";
23
+ import { DocumentLiveStatus, DocumentLoadStatus } from "../types/DocumentService.js";
24
+ import { createTestApp } from "./testUtils.js";
25
+
26
+ const DOCUMENT_ID = "test-doc" as DocumentId;
27
+
28
+ const testSchema = {
29
+ [Metadata]: {
30
+ version: 1,
31
+ },
32
+ } as const satisfies DocumentSchema;
33
+
34
+ describe("Document Status Tracking", () => {
35
+ it("should initialize with correct default status", () => {
36
+ const mockApp = createTestApp();
37
+ const service = internalCreateInMemoryDocumentService(mockApp, { autoCreateDocuments: true });
38
+ const docRef = createDocRef(mockApp, DOCUMENT_ID, testSchema);
39
+
40
+ const status = service.getDocumentStatus(docRef);
41
+
42
+ expect(status.metadata.load).toBe(DocumentLoadStatus.UNLOADED);
43
+ expect(status.metadata.live).toBe(DocumentLiveStatus.DISCONNECTED);
44
+ expect(status.data.load).toBe(DocumentLoadStatus.UNLOADED);
45
+ expect(status.data.live).toBe(DocumentLiveStatus.DISCONNECTED);
46
+ expect(status.metadataError).toBeUndefined();
47
+ expect(status.dataError).toBeUndefined();
48
+ });
49
+
50
+ it("should trigger metadata loading on first metadata subscription", async () => {
51
+ const mockApp = createTestApp();
52
+ const service = internalCreateInMemoryDocumentService(mockApp, { autoCreateDocuments: true });
53
+ const docRef = createDocRef(mockApp, DOCUMENT_ID, testSchema);
54
+
55
+ const statusUpdates: DocumentStatus[] = [];
56
+
57
+ // Subscribe to metadata changes first (triggers loading)
58
+ const unsubscribeMetadata = service.onMetadataChange(docRef, () => {});
59
+
60
+ // Then subscribe to status changes (might already be loaded)
61
+ const unsubscribeStatus = service.onStatusChange(docRef, (_, status) => {
62
+ statusUpdates.push(status);
63
+ });
64
+
65
+ // Wait for async loading to complete
66
+ await new Promise(resolve => setTimeout(resolve, 0));
67
+
68
+ // The first status we receive should be loaded (since InMemory + autoCreate loads immediately)
69
+ expect(statusUpdates.length).toBeGreaterThanOrEqual(1);
70
+ const finalStatus = statusUpdates.at(-1);
71
+ expect(finalStatus?.metadata.load).toBe(DocumentLoadStatus.LOADED);
72
+
73
+ unsubscribeStatus();
74
+ unsubscribeMetadata();
75
+ });
76
+
77
+ it("should trigger data loading on first data subscription", async () => {
78
+ const mockApp = createTestApp();
79
+ const service = internalCreateInMemoryDocumentService(mockApp, { autoCreateDocuments: true });
80
+ const docRef = createDocRef(mockApp, DOCUMENT_ID, testSchema);
81
+
82
+ const statusUpdates: DocumentStatus[] = [];
83
+
84
+ // Subscribe to state changes first (triggers data loading)
85
+ const unsubscribeState = service.onStateChange(docRef, () => {});
86
+
87
+ // Then subscribe to status changes (might already be loaded)
88
+ const unsubscribeStatus = service.onStatusChange(docRef, (_, status) => {
89
+ statusUpdates.push(status);
90
+ });
91
+
92
+ // Wait for async loading to complete
93
+ await new Promise(resolve => setTimeout(resolve, 0));
94
+
95
+ // The first status we receive should be loaded (since InMemory + autoCreate loads immediately)
96
+ expect(statusUpdates.length).toBeGreaterThanOrEqual(1);
97
+ const finalStatus = statusUpdates.at(-1);
98
+ expect(finalStatus?.data.load).toBe(DocumentLoadStatus.LOADED);
99
+
100
+ unsubscribeStatus();
101
+ unsubscribeState();
102
+ });
103
+
104
+ it("should not reload if already loaded", async () => {
105
+ const mockApp = createTestApp();
106
+ const service = internalCreateInMemoryDocumentService(mockApp, { autoCreateDocuments: true });
107
+ const docRef = createDocRef(mockApp, DOCUMENT_ID, testSchema);
108
+
109
+ // First subscription
110
+ const unsubscribe1 = service.onMetadataChange(docRef, () => {});
111
+ await new Promise(resolve => setTimeout(resolve, 0));
112
+
113
+ const statusUpdates: DocumentStatus[] = [];
114
+
115
+ // Subscribe to status changes after first load
116
+ const unsubscribeStatus = service.onStatusChange(docRef, (_, status) => {
117
+ statusUpdates.push(status);
118
+ });
119
+
120
+ // Second subscription should not trigger another load
121
+ const unsubscribe2 = service.onMetadataChange(docRef, () => {});
122
+ await new Promise(resolve => setTimeout(resolve, 0));
123
+
124
+ // Should only get the initial status (already loaded)
125
+ expect(statusUpdates).toHaveLength(1);
126
+ expect(statusUpdates[0]?.metadata.load).toBe(DocumentLoadStatus.LOADED);
127
+
128
+ unsubscribe1();
129
+ unsubscribe2();
130
+ unsubscribeStatus();
131
+ });
132
+
133
+ it("should handle subscription lifecycle correctly", async () => {
134
+ const mockApp = createTestApp();
135
+ const service = internalCreateInMemoryDocumentService(mockApp, { autoCreateDocuments: true });
136
+ const docRef = createDocRef(mockApp, DOCUMENT_ID, testSchema);
137
+
138
+ // First subscription
139
+ const unsubscribe1 = service.onMetadataChange(docRef, () => {});
140
+ await new Promise(resolve => setTimeout(resolve, 0));
141
+
142
+ // Second subscription
143
+ const unsubscribe2 = service.onMetadataChange(docRef, () => {});
144
+
145
+ // Unsubscribe first (should not close subscription)
146
+ unsubscribe1();
147
+
148
+ const status1 = service.getDocumentStatus(docRef);
149
+ expect(status1.metadata.load).toBe(DocumentLoadStatus.LOADED);
150
+
151
+ // Unsubscribe last (should close subscription but keep loaded state)
152
+ unsubscribe2();
153
+
154
+ const status2 = service.getDocumentStatus(docRef);
155
+ expect(status2.metadata.load).toBe(DocumentLoadStatus.LOADED);
156
+ });
157
+
158
+ it("should wait for metadata load", async () => {
159
+ const mockApp = createTestApp();
160
+ const service = internalCreateInMemoryDocumentService(mockApp, { autoCreateDocuments: true });
161
+ const docRef = createDocRef(mockApp, DOCUMENT_ID, testSchema);
162
+
163
+ // Start metadata subscription in background
164
+ const unsubscribe = service.onMetadataChange(docRef, () => {});
165
+
166
+ // Wait for load should resolve
167
+ await expect(service.waitForMetadataLoad(docRef)).resolves.toBeUndefined();
168
+
169
+ unsubscribe();
170
+ });
171
+
172
+ it("should wait for data load", async () => {
173
+ const mockApp = createTestApp();
174
+ const service = internalCreateInMemoryDocumentService(mockApp, { autoCreateDocuments: true });
175
+ const docRef = createDocRef(mockApp, DOCUMENT_ID, testSchema);
176
+
177
+ // Start data subscription in background
178
+ const unsubscribe = service.onStateChange(docRef, () => {});
179
+
180
+ // Wait for load should resolve
181
+ await expect(service.waitForDataLoad(docRef)).resolves.toBeUndefined();
182
+
183
+ unsubscribe();
184
+ });
185
+
186
+ it("should handle multiple subscription types for data loading", async () => {
187
+ const mockApp = createTestApp();
188
+ const service = internalCreateInMemoryDocumentService(mockApp, { autoCreateDocuments: true });
189
+ const docRef = createDocRef(mockApp, DOCUMENT_ID, testSchema);
190
+
191
+ // First state subscription
192
+ const unsubscribeState1 = service.onStateChange(docRef, () => {});
193
+ await new Promise(resolve => setTimeout(resolve, 0));
194
+
195
+ const statusUpdates: DocumentStatus[] = [];
196
+ const unsubscribeStatus = service.onStatusChange(docRef, (_, status) => {
197
+ statusUpdates.push(status);
198
+ });
199
+
200
+ // Second state subscription should not trigger another load
201
+ const unsubscribeState2 = service.onStateChange(docRef, () => {});
202
+ await new Promise(resolve => setTimeout(resolve, 0));
203
+
204
+ // Since the document was already loaded before status subscription, we should only see loaded state
205
+ expect(statusUpdates.length).toBeGreaterThanOrEqual(1);
206
+ const allLoadedStatuses = statusUpdates.every(s => s.data.load === DocumentLoadStatus.LOADED);
207
+ expect(allLoadedStatuses).toBe(true);
208
+
209
+ unsubscribeState1();
210
+ unsubscribeState2();
211
+ unsubscribeStatus();
212
+ });
213
+
214
+ it("should provide immediate status to new subscribers", () => {
215
+ const mockApp = createTestApp();
216
+ const service = internalCreateInMemoryDocumentService(mockApp, { autoCreateDocuments: true });
217
+ const docRef = createDocRef(mockApp, DOCUMENT_ID, testSchema);
218
+
219
+ let immediateStatus: DocumentStatus | null = null;
220
+
221
+ service.onStatusChange(docRef, (_, status) => {
222
+ immediateStatus = status;
223
+ });
224
+
225
+ expect(immediateStatus).toBeTruthy();
226
+ expect(immediateStatus!.metadata.load).toBe(DocumentLoadStatus.UNLOADED);
227
+ expect(immediateStatus!.data.load).toBe(DocumentLoadStatus.UNLOADED);
228
+ });
229
+ });