@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.
- package/.turbo/turbo-lint.log +4 -0
- package/.turbo/turbo-transpileBrowser.log +5 -0
- package/.turbo/turbo-transpileCjs.log +5 -0
- package/.turbo/turbo-transpileEsm.log +5 -0
- package/.turbo/turbo-transpileTypes.log +5 -0
- package/.turbo/turbo-typecheck.log +4 -0
- package/LICENSE.txt +13 -0
- package/README.md +55 -0
- package/build/browser/index.js +1257 -0
- package/build/browser/index.js.map +1 -0
- package/build/cjs/index.cjs +1298 -0
- package/build/cjs/index.cjs.map +1 -0
- package/build/cjs/index.d.cts +272 -0
- package/build/esm/index.js +1257 -0
- package/build/esm/index.js.map +1 -0
- package/build/types/DocumentServiceModule.d.ts +6 -0
- package/build/types/DocumentServiceModule.d.ts.map +1 -0
- package/build/types/__tests__/DocumentStatusTracking.test.d.ts +1 -0
- package/build/types/__tests__/DocumentStatusTracking.test.d.ts.map +1 -0
- package/build/types/__tests__/RefStability.test.d.ts +1 -0
- package/build/types/__tests__/RefStability.test.d.ts.map +1 -0
- package/build/types/__tests__/StateModule.integration.test.d.ts +1 -0
- package/build/types/__tests__/StateModule.integration.test.d.ts.map +1 -0
- package/build/types/__tests__/testUtils.d.ts +7 -0
- package/build/types/__tests__/testUtils.d.ts.map +1 -0
- package/build/types/index.d.ts +11 -0
- package/build/types/index.d.ts.map +1 -0
- package/build/types/service/BaseYjsDocumentService.d.ts +155 -0
- package/build/types/service/BaseYjsDocumentService.d.ts.map +1 -0
- package/build/types/service/InMemoryDocumentService.d.ts +12 -0
- package/build/types/service/InMemoryDocumentService.d.ts.map +1 -0
- package/build/types/service/YjsSchemaMapper.d.ts +9 -0
- package/build/types/service/YjsSchemaMapper.d.ts.map +1 -0
- package/build/types/types/DocumentRefImpl.d.ts +5 -0
- package/build/types/types/DocumentRefImpl.d.ts.map +1 -0
- package/build/types/types/DocumentService.d.ts +62 -0
- package/build/types/types/DocumentService.d.ts.map +1 -0
- package/build/types/types/DocumentServiceConfig.d.ts +5 -0
- package/build/types/types/DocumentServiceConfig.d.ts.map +1 -0
- package/build/types/types/RecordCollectionRefImpl.d.ts +5 -0
- package/build/types/types/RecordCollectionRefImpl.d.ts.map +1 -0
- package/build/types/types/RecordRefImpl.d.ts +5 -0
- package/build/types/types/RecordRefImpl.d.ts.map +1 -0
- package/build/types/types/StateModule.d.ts +59 -0
- package/build/types/types/StateModule.d.ts.map +1 -0
- package/package.json +71 -0
- package/src/DocumentServiceModule.ts +53 -0
- package/src/__tests__/DocumentStatusTracking.test.ts +229 -0
- package/src/__tests__/RefStability.test.ts +441 -0
- package/src/__tests__/StateModule.integration.test.ts +1187 -0
- package/src/__tests__/testUtils.ts +106 -0
- package/src/index.ts +38 -0
- package/src/service/BaseYjsDocumentService.ts +1277 -0
- package/src/service/InMemoryDocumentService.ts +162 -0
- package/src/service/YjsSchemaMapper.ts +194 -0
- package/src/types/DocumentRefImpl.ts +98 -0
- package/src/types/DocumentService.ts +210 -0
- package/src/types/DocumentServiceConfig.ts +22 -0
- package/src/types/RecordCollectionRefImpl.ts +124 -0
- package/src/types/RecordRefImpl.ts +106 -0
- package/src/types/StateModule.ts +329 -0
- package/tsconfig.json +21 -0
- package/vitest.config.mjs +26 -0
|
@@ -0,0 +1,162 @@
|
|
|
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, PackAppInternal } from "@palantir/pack.core";
|
|
18
|
+
import { generateId } from "@palantir/pack.core";
|
|
19
|
+
import type {
|
|
20
|
+
DocumentId,
|
|
21
|
+
DocumentMetadata,
|
|
22
|
+
DocumentRef,
|
|
23
|
+
DocumentSchema,
|
|
24
|
+
} from "@palantir/pack.document-schema.model-types";
|
|
25
|
+
import type * as Y from "yjs";
|
|
26
|
+
import { createDocumentServiceConfig } from "../DocumentServiceModule.js";
|
|
27
|
+
import { createDocRef } from "../types/DocumentRefImpl.js";
|
|
28
|
+
import type { DocumentService } from "../types/DocumentService.js";
|
|
29
|
+
import { DocumentLoadStatus } from "../types/DocumentService.js";
|
|
30
|
+
import type { InternalYjsDoc } from "./BaseYjsDocumentService.js";
|
|
31
|
+
import { BaseYjsDocumentService } from "./BaseYjsDocumentService.js";
|
|
32
|
+
|
|
33
|
+
export interface InMemoryDocumentServiceOptions {
|
|
34
|
+
/**
|
|
35
|
+
* Automatically create documents when referenced
|
|
36
|
+
* @default true
|
|
37
|
+
*/
|
|
38
|
+
readonly autoCreateDocuments?: boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function createInMemoryDocumentServiceConfig(
|
|
42
|
+
{ autoCreateDocuments = true }: InMemoryDocumentServiceOptions = {},
|
|
43
|
+
): ModuleConfigTuple<DocumentService> {
|
|
44
|
+
const config: InMemoryDocumentServiceOptions = {
|
|
45
|
+
autoCreateDocuments,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
return createDocumentServiceConfig(internalCreateInMemoryDocumentService, config);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function internalCreateInMemoryDocumentService(
|
|
52
|
+
app: PackAppInternal,
|
|
53
|
+
options: InMemoryDocumentServiceOptions,
|
|
54
|
+
): BaseYjsDocumentService {
|
|
55
|
+
return new InMemoryDocumentService(app, options);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
class InMemoryDocumentService extends BaseYjsDocumentService {
|
|
59
|
+
constructor(
|
|
60
|
+
app: PackAppInternal,
|
|
61
|
+
readonly config: InMemoryDocumentServiceOptions,
|
|
62
|
+
) {
|
|
63
|
+
super(
|
|
64
|
+
app,
|
|
65
|
+
app.config.logger.child({}, { level: "debug", msgPrefix: "InMemoryDocumentService" }),
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
override createInternalDoc(
|
|
70
|
+
ref: DocumentRef,
|
|
71
|
+
metadata?: DocumentMetadata,
|
|
72
|
+
yDoc?: Y.Doc,
|
|
73
|
+
): InternalYjsDoc {
|
|
74
|
+
return this.createBaseInternalDoc(ref, metadata, yDoc);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
get hasMetadataSubscriptions() {
|
|
78
|
+
return Array.from(this.documents.values()).some(doc =>
|
|
79
|
+
this.hasSubscriptions(doc) && doc.metadataSubscribers.size > 0
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
get hasStateSubscriptions() {
|
|
84
|
+
return Array.from(this.documents.values()).some(doc =>
|
|
85
|
+
this.hasSubscriptions(doc) && doc.docStateSubscribers.size > 0
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
readonly createDocument = <T extends DocumentSchema>(
|
|
90
|
+
metadata: DocumentMetadata,
|
|
91
|
+
schema: T,
|
|
92
|
+
): Promise<DocumentRef<T>> => {
|
|
93
|
+
const id = generateDocumentId();
|
|
94
|
+
const docRef = createDocRef(this.app, id, schema);
|
|
95
|
+
|
|
96
|
+
const yDoc = this.initializeYDoc(schema);
|
|
97
|
+
this.getCreateInternalDoc(docRef, metadata, yDoc);
|
|
98
|
+
|
|
99
|
+
return Promise.resolve(docRef);
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
// Lifecycle method implementations
|
|
103
|
+
protected onMetadataSubscriptionOpened(
|
|
104
|
+
internalDoc: InternalYjsDoc,
|
|
105
|
+
docRef: DocumentRef,
|
|
106
|
+
): void {
|
|
107
|
+
this.updateMetadataStatus(internalDoc, docRef, {
|
|
108
|
+
load: DocumentLoadStatus.LOADING,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
if (this.config.autoCreateDocuments === false && internalDoc.metadata == null) {
|
|
112
|
+
this.updateMetadataStatus(internalDoc, docRef, {
|
|
113
|
+
error: new Error("Document not found and autoCreateDocuments is disabled"),
|
|
114
|
+
load: DocumentLoadStatus.ERROR,
|
|
115
|
+
});
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
this.updateMetadataStatus(internalDoc, docRef, {
|
|
120
|
+
load: DocumentLoadStatus.LOADED,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
protected onDataSubscriptionOpened(
|
|
125
|
+
internalDoc: InternalYjsDoc,
|
|
126
|
+
docRef: DocumentRef,
|
|
127
|
+
): void {
|
|
128
|
+
this.updateDataStatus(internalDoc, docRef, {
|
|
129
|
+
load: DocumentLoadStatus.LOADING,
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
if (this.config.autoCreateDocuments === false && internalDoc.metadata == null) {
|
|
133
|
+
this.updateDataStatus(internalDoc, docRef, {
|
|
134
|
+
error: new Error("Document not found and autoCreateDocuments is disabled"),
|
|
135
|
+
load: DocumentLoadStatus.ERROR,
|
|
136
|
+
});
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
this.updateDataStatus(internalDoc, docRef, {
|
|
141
|
+
load: DocumentLoadStatus.LOADED,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
protected onMetadataSubscriptionClosed(
|
|
146
|
+
_internalDoc: InternalYjsDoc,
|
|
147
|
+
_docRef: DocumentRef,
|
|
148
|
+
): void {
|
|
149
|
+
// No cleanup needed for in-memory service
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
protected onDataSubscriptionClosed(
|
|
153
|
+
_internalDoc: InternalYjsDoc,
|
|
154
|
+
_docRef: DocumentRef,
|
|
155
|
+
): void {
|
|
156
|
+
// No cleanup needed for in-memory service
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function generateDocumentId(): DocumentId {
|
|
161
|
+
return generateId() as DocumentId;
|
|
162
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
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 {
|
|
18
|
+
type DocumentSchema,
|
|
19
|
+
getMetadata,
|
|
20
|
+
type Model,
|
|
21
|
+
type ModelData,
|
|
22
|
+
type RecordId,
|
|
23
|
+
} from "@palantir/pack.document-schema.model-types";
|
|
24
|
+
import * as Y from "yjs";
|
|
25
|
+
|
|
26
|
+
export function initializeDocumentStructure(
|
|
27
|
+
yDoc: Y.Doc,
|
|
28
|
+
schema: DocumentSchema,
|
|
29
|
+
): void {
|
|
30
|
+
// Initialize storage for each model type using storageName directly on the root Y.Doc
|
|
31
|
+
Object.values(schema).forEach(modelEntry => {
|
|
32
|
+
yDoc.getMap(getMetadata(modelEntry).name);
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function getRecordsMap(
|
|
37
|
+
yDoc: Y.Doc,
|
|
38
|
+
storageName: string,
|
|
39
|
+
): Y.Map<unknown> {
|
|
40
|
+
return yDoc.getMap(storageName);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function getRecordData(
|
|
44
|
+
yDoc: Y.Doc,
|
|
45
|
+
storageName: string,
|
|
46
|
+
recordId: RecordId,
|
|
47
|
+
): Y.Map<unknown> | undefined {
|
|
48
|
+
const recordsCollection = getRecordsMap(yDoc, storageName);
|
|
49
|
+
return recordsCollection.get(recordId as string) as Y.Map<unknown> | undefined;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function setRecord(
|
|
53
|
+
yDoc: Y.Doc,
|
|
54
|
+
storageName: string,
|
|
55
|
+
recordId: RecordId,
|
|
56
|
+
state: ModelData<Model>,
|
|
57
|
+
): boolean {
|
|
58
|
+
const recordsCollection = getRecordsMap(yDoc, storageName);
|
|
59
|
+
const currentRecord = recordsCollection.get(recordId as string) as Y.Map<unknown> | undefined;
|
|
60
|
+
const wasExisting = currentRecord != null;
|
|
61
|
+
|
|
62
|
+
// Use transaction for atomic update
|
|
63
|
+
yDoc.transact(() => {
|
|
64
|
+
if (currentRecord != null) {
|
|
65
|
+
// Clear existing record completely
|
|
66
|
+
currentRecord.clear();
|
|
67
|
+
populateYMapFromState(currentRecord, state);
|
|
68
|
+
} else {
|
|
69
|
+
// Create new record
|
|
70
|
+
const newRecord = new Y.Map();
|
|
71
|
+
populateYMapFromState(newRecord, state);
|
|
72
|
+
recordsCollection.set(recordId as string, newRecord);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
return wasExisting;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function getRecordSnapshot(
|
|
80
|
+
yDoc: Y.Doc,
|
|
81
|
+
storageName: string,
|
|
82
|
+
recordId: RecordId,
|
|
83
|
+
): unknown {
|
|
84
|
+
const data = getRecordData(yDoc, storageName, recordId);
|
|
85
|
+
if (!data) {
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return yMapToState(data);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function updateRecord(
|
|
93
|
+
yDoc: Y.Doc,
|
|
94
|
+
storageName: string,
|
|
95
|
+
recordId: RecordId,
|
|
96
|
+
partialState: Partial<ModelData<Model>>,
|
|
97
|
+
): boolean {
|
|
98
|
+
const recordsCollection = getRecordsMap(yDoc, storageName);
|
|
99
|
+
const currentRecord = recordsCollection.get(recordId as string) as Y.Map<unknown> | undefined;
|
|
100
|
+
|
|
101
|
+
if (currentRecord == null) {
|
|
102
|
+
return false; // Record doesn't exist, cannot update
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Use transaction for atomic partial update
|
|
106
|
+
yDoc.transact(() => {
|
|
107
|
+
updateYMapFromPartialState(currentRecord, partialState);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function getAllRecordIds(yDoc: Y.Doc, storageName: string): RecordId[] {
|
|
114
|
+
const recordsCollection = getRecordsMap(yDoc, storageName);
|
|
115
|
+
return Array.from(recordsCollection.keys()).map(key => key as RecordId);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function populateYMapFromState(
|
|
119
|
+
yMap: Y.Map<unknown>,
|
|
120
|
+
state: unknown,
|
|
121
|
+
): void {
|
|
122
|
+
if (state != null && typeof state === "object") {
|
|
123
|
+
Object.entries(state).forEach(([key, value]) => {
|
|
124
|
+
if (value === undefined) return;
|
|
125
|
+
|
|
126
|
+
if (Array.isArray(value)) {
|
|
127
|
+
const yArray = new Y.Array();
|
|
128
|
+
value.forEach(item => {
|
|
129
|
+
yArray.push([item]);
|
|
130
|
+
});
|
|
131
|
+
yMap.set(key, yArray);
|
|
132
|
+
} else if (typeof value === "object" && value != null) {
|
|
133
|
+
const nestedMap = new Y.Map();
|
|
134
|
+
populateYMapFromState(nestedMap, value as Model);
|
|
135
|
+
yMap.set(key, nestedMap);
|
|
136
|
+
} else {
|
|
137
|
+
yMap.set(key, value);
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function updateYMapFromPartialState(
|
|
144
|
+
yMap: Y.Map<unknown>,
|
|
145
|
+
partialState: Partial<unknown>,
|
|
146
|
+
): void {
|
|
147
|
+
if (typeof partialState === "object") {
|
|
148
|
+
Object.entries(partialState).forEach(([key, value]) => {
|
|
149
|
+
if (value === undefined) {
|
|
150
|
+
// Remove the field if undefined is explicitly provided
|
|
151
|
+
yMap.delete(key);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (Array.isArray(value)) {
|
|
156
|
+
const yArray = new Y.Array();
|
|
157
|
+
value.forEach(item => {
|
|
158
|
+
yArray.push([item]);
|
|
159
|
+
});
|
|
160
|
+
yMap.set(key, yArray);
|
|
161
|
+
} else if (typeof value === "object" && value != null) {
|
|
162
|
+
// For nested objects, we need to handle partial updates
|
|
163
|
+
const existingValue = yMap.get(key);
|
|
164
|
+
if (existingValue instanceof Y.Map) {
|
|
165
|
+
// Recursively update nested Y.Map
|
|
166
|
+
updateYMapFromPartialState(existingValue, value as Partial<unknown>);
|
|
167
|
+
} else {
|
|
168
|
+
// Replace with new nested map if existing value is not a Y.Map
|
|
169
|
+
const nestedMap = new Y.Map();
|
|
170
|
+
populateYMapFromState(nestedMap, value as Model);
|
|
171
|
+
yMap.set(key, nestedMap);
|
|
172
|
+
}
|
|
173
|
+
} else {
|
|
174
|
+
yMap.set(key, value);
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function yMapToState(yMap: Y.Map<unknown>): ModelData<Model> {
|
|
181
|
+
const state: Record<string, unknown> = {};
|
|
182
|
+
|
|
183
|
+
yMap.forEach((value, key) => {
|
|
184
|
+
if (value instanceof Y.Array) {
|
|
185
|
+
state[key] = value.toArray();
|
|
186
|
+
} else if (value instanceof Y.Map) {
|
|
187
|
+
state[key] = yMapToState(value);
|
|
188
|
+
} else {
|
|
189
|
+
state[key] = value;
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
return state as ModelData<Model>;
|
|
194
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
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 } from "@palantir/pack.core";
|
|
18
|
+
import type {
|
|
19
|
+
DocumentId,
|
|
20
|
+
DocumentMetadata,
|
|
21
|
+
DocumentRef,
|
|
22
|
+
DocumentSchema,
|
|
23
|
+
DocumentState,
|
|
24
|
+
Model,
|
|
25
|
+
RecordCollectionRef,
|
|
26
|
+
Unsubscribe,
|
|
27
|
+
} from "@palantir/pack.document-schema.model-types";
|
|
28
|
+
import { DocumentRefBrand } from "@palantir/pack.document-schema.model-types";
|
|
29
|
+
import type { StateModuleImpl } from "./StateModule.js";
|
|
30
|
+
import { getStateModule } from "./StateModule.js";
|
|
31
|
+
|
|
32
|
+
const INVALID_DOC_REF_ID: DocumentId = "INVALID_DOC_REF";
|
|
33
|
+
const INVALID_DOC_REF: DocumentRef = Object.freeze(
|
|
34
|
+
{
|
|
35
|
+
id: INVALID_DOC_REF_ID,
|
|
36
|
+
schema: {} as DocumentSchema,
|
|
37
|
+
[DocumentRefBrand]: DocumentRefBrand,
|
|
38
|
+
getDocSnapshot: () => Promise.reject(new Error("Invalid document reference")),
|
|
39
|
+
getRecords: () => {
|
|
40
|
+
throw new Error("Invalid document reference");
|
|
41
|
+
},
|
|
42
|
+
onMetadataChange: () => () => {},
|
|
43
|
+
onStateChange: () => () => {},
|
|
44
|
+
} as const,
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
export const createDocRef = <const D extends DocumentSchema>(
|
|
48
|
+
app: PackAppInternal,
|
|
49
|
+
id: DocumentId,
|
|
50
|
+
schema: D,
|
|
51
|
+
): DocumentRef<D> => {
|
|
52
|
+
return new DocumentRefImpl(app, id, schema);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export function invalidDocRef<D extends DocumentSchema = DocumentSchema>(): DocumentRef<D> {
|
|
56
|
+
return INVALID_DOC_REF as DocumentRef<D>;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function isValidDocRef<D extends DocumentSchema = DocumentSchema>(
|
|
60
|
+
docRef: DocumentRef<D>,
|
|
61
|
+
): docRef is DocumentRef<D> {
|
|
62
|
+
return docRef.id !== INVALID_DOC_REF_ID && docRef.id !== "";
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
class DocumentRefImpl<T extends DocumentSchema> implements DocumentRef<T> {
|
|
66
|
+
readonly id: DocumentId;
|
|
67
|
+
readonly schema: T;
|
|
68
|
+
declare readonly [DocumentRefBrand]: typeof DocumentRefBrand;
|
|
69
|
+
readonly #stateModule: StateModuleImpl;
|
|
70
|
+
|
|
71
|
+
constructor(app: PackAppInternal, id: DocumentId, schema: T) {
|
|
72
|
+
this.#stateModule = getStateModule(app) as StateModuleImpl;
|
|
73
|
+
this.id = id;
|
|
74
|
+
this.schema = schema;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async getDocSnapshot(): Promise<DocumentState<T>> {
|
|
78
|
+
return this.#stateModule.getDocumentSnapshot(this);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
getRecords<R extends Model>(
|
|
82
|
+
model: R,
|
|
83
|
+
): RecordCollectionRef<R> {
|
|
84
|
+
return this.#stateModule.getCreateRecordCollectionRef(this, model);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
onMetadataChange(
|
|
88
|
+
cb: (docRef: DocumentRef<T>, metadata: DocumentMetadata) => void,
|
|
89
|
+
): Unsubscribe {
|
|
90
|
+
return this.#stateModule.onMetadataChange(this, cb);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
onStateChange(
|
|
94
|
+
callback: (docRef: DocumentRef<T>) => void,
|
|
95
|
+
): Unsubscribe {
|
|
96
|
+
return this.#stateModule.onStateChange(this, callback);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
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 { Unsubscribe } from "@palantir/pack.core";
|
|
18
|
+
import type {
|
|
19
|
+
DocumentId,
|
|
20
|
+
DocumentMetadata,
|
|
21
|
+
DocumentRef,
|
|
22
|
+
DocumentSchema,
|
|
23
|
+
DocumentState,
|
|
24
|
+
Model,
|
|
25
|
+
ModelData,
|
|
26
|
+
RecordCollectionRef,
|
|
27
|
+
RecordId,
|
|
28
|
+
RecordRef,
|
|
29
|
+
} from "@palantir/pack.document-schema.model-types";
|
|
30
|
+
|
|
31
|
+
export const DocumentLoadStatus = {
|
|
32
|
+
UNLOADED: "unloaded", // Not yet loaded
|
|
33
|
+
LOADING: "loading", // Initial load in progress
|
|
34
|
+
LOADED: "loaded", // Successfully loaded
|
|
35
|
+
ERROR: "error", // Load failed
|
|
36
|
+
} as const;
|
|
37
|
+
export type DocumentLoadStatus = typeof DocumentLoadStatus[keyof typeof DocumentLoadStatus];
|
|
38
|
+
|
|
39
|
+
export const DocumentLiveStatus = {
|
|
40
|
+
DISCONNECTED: "disconnected", // Not syncing
|
|
41
|
+
CONNECTING: "connecting", // Establishing connection
|
|
42
|
+
CONNECTED: "connected", // Live syncing active
|
|
43
|
+
ERROR: "error", // Connection error
|
|
44
|
+
} as const;
|
|
45
|
+
export type DocumentLiveStatus = typeof DocumentLiveStatus[keyof typeof DocumentLiveStatus];
|
|
46
|
+
|
|
47
|
+
export type DocumentSyncStatus = {
|
|
48
|
+
readonly error?: unknown;
|
|
49
|
+
readonly live: DocumentLiveStatus;
|
|
50
|
+
readonly load: DocumentLoadStatus;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export type DocumentStatus = {
|
|
54
|
+
readonly metadata: DocumentSyncStatus;
|
|
55
|
+
readonly data: DocumentSyncStatus;
|
|
56
|
+
readonly metadataError?: unknown;
|
|
57
|
+
readonly dataError?: unknown;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export type DocumentStatusChangeCallback = (
|
|
61
|
+
docRef: DocumentRef,
|
|
62
|
+
status: DocumentStatus,
|
|
63
|
+
) => void;
|
|
64
|
+
|
|
65
|
+
export type DocumentMetadataChangeCallback<
|
|
66
|
+
T extends DocumentSchema = DocumentSchema,
|
|
67
|
+
> = (docRef: DocumentRef<T>, metadata: DocumentMetadata) => void;
|
|
68
|
+
|
|
69
|
+
export type DocumentStateChangeCallback<
|
|
70
|
+
T extends DocumentSchema = DocumentSchema,
|
|
71
|
+
> = (docRef: DocumentRef<T>) => void;
|
|
72
|
+
|
|
73
|
+
export type RecordCollectionChangeCallback<M extends Model = Model> = (
|
|
74
|
+
items: readonly RecordRef<M>[],
|
|
75
|
+
) => void;
|
|
76
|
+
|
|
77
|
+
export type RecordChangeCallback<M extends Model = Model> = (
|
|
78
|
+
snapshot: ModelData<M>,
|
|
79
|
+
record: RecordRef<M>,
|
|
80
|
+
) => void;
|
|
81
|
+
|
|
82
|
+
export type RecordDeleteCallback<M extends Model = Model> = (
|
|
83
|
+
record: RecordRef<M>,
|
|
84
|
+
) => void;
|
|
85
|
+
|
|
86
|
+
export interface DocumentService {
|
|
87
|
+
readonly hasMetadataSubscriptions: boolean;
|
|
88
|
+
readonly hasStateSubscriptions: boolean;
|
|
89
|
+
|
|
90
|
+
readonly createDocument: <T extends DocumentSchema>(
|
|
91
|
+
metadata: DocumentMetadata,
|
|
92
|
+
schema: T,
|
|
93
|
+
) => Promise<DocumentRef<T>>;
|
|
94
|
+
|
|
95
|
+
readonly createDocRef: <const T extends DocumentSchema>(
|
|
96
|
+
id: DocumentId,
|
|
97
|
+
schema: T,
|
|
98
|
+
) => DocumentRef<T>;
|
|
99
|
+
|
|
100
|
+
readonly getCreateRecordCollectionRef: <const M extends Model>(
|
|
101
|
+
docRef: DocumentRef,
|
|
102
|
+
model: M,
|
|
103
|
+
) => RecordCollectionRef<M>;
|
|
104
|
+
|
|
105
|
+
readonly getCreateRecordRef: <const M extends Model>(
|
|
106
|
+
docRef: DocumentRef,
|
|
107
|
+
id: RecordId,
|
|
108
|
+
model: M,
|
|
109
|
+
) => RecordRef<M>;
|
|
110
|
+
|
|
111
|
+
readonly getDocumentSnapshot: <T extends DocumentSchema>(
|
|
112
|
+
docRef: DocumentRef<T>,
|
|
113
|
+
) => Promise<DocumentState<T>>;
|
|
114
|
+
|
|
115
|
+
readonly getRecordSnapshot: <R extends Model>(
|
|
116
|
+
record: RecordRef<R>,
|
|
117
|
+
) => Promise<ModelData<R>>;
|
|
118
|
+
|
|
119
|
+
readonly setRecord: <R extends Model>(
|
|
120
|
+
record: RecordRef<R>,
|
|
121
|
+
state: ModelData<R>,
|
|
122
|
+
) => Promise<void>;
|
|
123
|
+
|
|
124
|
+
readonly updateRecord: <R extends Model>(
|
|
125
|
+
record: RecordRef<R>,
|
|
126
|
+
partialState: Partial<ModelData<R>>,
|
|
127
|
+
) => Promise<void>;
|
|
128
|
+
|
|
129
|
+
// Collection methods
|
|
130
|
+
readonly getRecord: <M extends Model>(
|
|
131
|
+
collection: RecordCollectionRef<M>,
|
|
132
|
+
id: RecordId,
|
|
133
|
+
) => RecordRef<M> | undefined;
|
|
134
|
+
|
|
135
|
+
readonly hasRecord: <M extends Model>(
|
|
136
|
+
collection: RecordCollectionRef<M>,
|
|
137
|
+
id: RecordId,
|
|
138
|
+
) => boolean;
|
|
139
|
+
|
|
140
|
+
readonly setCollectionRecord: <M extends Model>(
|
|
141
|
+
collection: RecordCollectionRef<M>,
|
|
142
|
+
id: RecordId,
|
|
143
|
+
state: ModelData<M>,
|
|
144
|
+
) => Promise<void>;
|
|
145
|
+
|
|
146
|
+
readonly deleteRecord: <M extends Model>(
|
|
147
|
+
record: RecordRef<M>,
|
|
148
|
+
) => Promise<void>;
|
|
149
|
+
|
|
150
|
+
readonly getCollectionSize: <M extends Model>(
|
|
151
|
+
collection: RecordCollectionRef<M>,
|
|
152
|
+
) => number;
|
|
153
|
+
|
|
154
|
+
readonly getCollectionRecords: <M extends Model>(
|
|
155
|
+
collection: RecordCollectionRef<M>,
|
|
156
|
+
) => RecordRef<M>[];
|
|
157
|
+
|
|
158
|
+
readonly onCollectionItemsAdded: <M extends Model>(
|
|
159
|
+
collection: RecordCollectionRef<M>,
|
|
160
|
+
callback: RecordCollectionChangeCallback<M>,
|
|
161
|
+
) => Unsubscribe;
|
|
162
|
+
|
|
163
|
+
readonly onCollectionItemsChanged: <M extends Model>(
|
|
164
|
+
collection: RecordCollectionRef<M>,
|
|
165
|
+
callback: RecordCollectionChangeCallback<M>,
|
|
166
|
+
) => Unsubscribe;
|
|
167
|
+
|
|
168
|
+
readonly onCollectionItemsDeleted: <M extends Model>(
|
|
169
|
+
collection: RecordCollectionRef<M>,
|
|
170
|
+
callback: RecordCollectionChangeCallback<M>,
|
|
171
|
+
) => Unsubscribe;
|
|
172
|
+
|
|
173
|
+
readonly onMetadataChange: <T extends DocumentSchema>(
|
|
174
|
+
docRef: DocumentRef<T>,
|
|
175
|
+
callback: DocumentMetadataChangeCallback<T>,
|
|
176
|
+
) => Unsubscribe;
|
|
177
|
+
|
|
178
|
+
readonly onStateChange: <T extends DocumentSchema>(
|
|
179
|
+
docRef: DocumentRef<T>,
|
|
180
|
+
callback: DocumentStateChangeCallback<T>,
|
|
181
|
+
) => Unsubscribe;
|
|
182
|
+
|
|
183
|
+
readonly onRecordChanged: <M extends Model>(
|
|
184
|
+
record: RecordRef<M>,
|
|
185
|
+
callback: RecordChangeCallback<M>,
|
|
186
|
+
) => Unsubscribe;
|
|
187
|
+
|
|
188
|
+
readonly onRecordDeleted: <M extends Model>(
|
|
189
|
+
record: RecordRef<M>,
|
|
190
|
+
callback: RecordDeleteCallback<M>,
|
|
191
|
+
) => Unsubscribe;
|
|
192
|
+
|
|
193
|
+
// Status methods
|
|
194
|
+
readonly getDocumentStatus: <T extends DocumentSchema>(
|
|
195
|
+
docRef: DocumentRef<T>,
|
|
196
|
+
) => DocumentStatus;
|
|
197
|
+
|
|
198
|
+
readonly onStatusChange: <T extends DocumentSchema>(
|
|
199
|
+
docRef: DocumentRef<T>,
|
|
200
|
+
callback: DocumentStatusChangeCallback,
|
|
201
|
+
) => Unsubscribe;
|
|
202
|
+
|
|
203
|
+
readonly waitForMetadataLoad: <T extends DocumentSchema>(
|
|
204
|
+
docRef: DocumentRef<T>,
|
|
205
|
+
) => Promise<void>;
|
|
206
|
+
|
|
207
|
+
readonly waitForDataLoad: <T extends DocumentSchema>(
|
|
208
|
+
docRef: DocumentRef<T>,
|
|
209
|
+
) => Promise<void>;
|
|
210
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
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 } from "@palantir/pack.core";
|
|
18
|
+
import type { DocumentService } from "./DocumentService.js";
|
|
19
|
+
|
|
20
|
+
export type WithDocumentServiceInit = {
|
|
21
|
+
readonly init: (app: PackAppInternal, config: unknown) => DocumentService;
|
|
22
|
+
};
|