@powerhousedao/reactor-browser 6.2.2-dev.8 → 6.2.2-dev.80
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/README.md +341 -0
- package/dist/attachment-service-LwpgZ358.d.ts +1057 -0
- package/dist/attachment-service-LwpgZ358.d.ts.map +1 -0
- package/dist/{client-u13cr-Vg.js → client-B8ezRmBB.js} +30 -3
- package/dist/client-B8ezRmBB.js.map +1 -0
- package/dist/client-BIHN_UyR.d.ts +14 -0
- package/dist/client-BIHN_UyR.d.ts.map +1 -0
- package/dist/{client-_Gh2Uf0E.js → client-D66jVnot.js} +79 -13
- package/dist/client-D66jVnot.js.map +1 -0
- package/dist/{document-by-id-dLYFX-xz.js → document-by-id-BSZqTN66.js} +81 -51
- package/dist/document-by-id-BSZqTN66.js.map +1 -0
- package/dist/document-model-modules-DMIR-uSD.js +411 -0
- package/dist/document-model-modules-DMIR-uSD.js.map +1 -0
- package/dist/document-operations-BO-SlTDw.js +2539 -0
- package/dist/document-operations-BO-SlTDw.js.map +1 -0
- package/dist/global-1rvI8VdA.d.ts +400 -0
- package/dist/global-1rvI8VdA.d.ts.map +1 -0
- package/dist/index-BE3wBHLP.d.ts +349 -0
- package/dist/index-BE3wBHLP.d.ts.map +1 -0
- package/dist/index-BIVDXw3K.d.ts +144 -0
- package/dist/index-BIVDXw3K.d.ts.map +1 -0
- package/dist/index.d.ts +313 -715
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +611 -1696
- package/dist/index.js.map +1 -1
- package/dist/{inspector-proxy-CvQDDxmT.d.ts → inspector-proxy-6h5VKAwt.d.ts} +4 -2
- package/dist/inspector-proxy-6h5VKAwt.d.ts.map +1 -0
- package/dist/{make-ph-event-functions-BHoLPif5.js → make-ph-event-functions-DBq3iWYn.js} +1 -1
- package/dist/{make-ph-event-functions-BHoLPif5.js.map → make-ph-event-functions-DBq3iWYn.js.map} +1 -1
- package/dist/{index-DS4W07Y8.d.ts → pglite-CntadC_p-9NDG14dS.d.ts} +2 -142
- package/dist/pglite-CntadC_p-9NDG14dS.d.ts.map +1 -0
- package/dist/{relational-Dj06-YkI.js → relational-D4sxGPz7.js} +2 -2
- package/dist/{relational-Dj06-YkI.js.map → relational-D4sxGPz7.js.map} +1 -1
- package/dist/renown-BHxwP7xU.js +97 -0
- package/dist/renown-BHxwP7xU.js.map +1 -0
- package/dist/{renown-D-1c_Bvx.js → renown-BIEv6bSI.js} +523 -57
- package/dist/renown-BIEv6bSI.js.map +1 -0
- package/dist/selected-document-xFBNPc9Q.js +42 -0
- package/dist/selected-document-xFBNPc9Q.js.map +1 -0
- package/dist/src/ai/index.d.ts +369 -0
- package/dist/src/ai/index.d.ts.map +1 -0
- package/dist/src/ai/index.js +1296 -0
- package/dist/src/ai/index.js.map +1 -0
- package/dist/src/analytics/index.d.ts +1 -1
- package/dist/src/analytics/index.js +2 -1
- package/dist/src/analytics/index.js.map +1 -1
- package/dist/src/graphql/client.d.ts +3 -2
- package/dist/src/graphql/client.js +1 -1
- package/dist/src/graphql-client/entry.d.ts +4 -0
- package/dist/src/graphql-client/entry.js +4 -0
- package/dist/src/relational/index.d.ts +1 -1
- package/dist/src/relational/index.js +1 -1
- package/dist/src/renown/index.d.ts +2 -2
- package/dist/src/renown/index.js +3 -2
- package/dist/src/rpc/index.d.ts +6 -3
- package/dist/src/rpc/index.d.ts.map +1 -1
- package/dist/src/rpc/index.js +41 -10
- package/dist/src/rpc/index.js.map +1 -1
- package/dist/store-BJYcr_FL.js +37 -0
- package/dist/store-BJYcr_FL.js.map +1 -0
- package/dist/{types-CPV0Cnsy.d.ts → types-6vDzbRRS.d.ts} +1 -1
- package/dist/{types-CPV0Cnsy.d.ts.map → types-6vDzbRRS.d.ts.map} +1 -1
- package/dist/{client-09xv0Dq6.d.ts → types-Dcv1ZLoW.d.ts} +87 -19
- package/dist/types-Dcv1ZLoW.d.ts.map +1 -0
- package/package.json +26 -10
- package/dist/client-09xv0Dq6.d.ts.map +0 -1
- package/dist/client-_Gh2Uf0E.js.map +0 -1
- package/dist/client-u13cr-Vg.js.map +0 -1
- package/dist/document-by-id-dLYFX-xz.js.map +0 -1
- package/dist/index-D3bV5rcl.d.ts +0 -216
- package/dist/index-D3bV5rcl.d.ts.map +0 -1
- package/dist/index-DS4W07Y8.d.ts.map +0 -1
- package/dist/inspector-proxy-CvQDDxmT.d.ts.map +0 -1
- package/dist/renown-D-1c_Bvx.js.map +0 -1
|
@@ -0,0 +1,2539 @@
|
|
|
1
|
+
import { S as uploadOperations, a as setDocumentCache, b as queueActions, g as DOCUMENT_CHANGE_TYPE, i as addDocumentCacheEventHandler, p as DocumentCache, x as queueOperations } from "./document-by-id-BSZqTN66.js";
|
|
2
|
+
import { C as addResetSelectedNodeEventHandler, D as addDrivesEventHandler, H as ambientRenownTokenProvider, T as addSetSelectedNodeOnPopStateEventHandler, U as makeAuthMiddleware, W as DocumentModelNotFoundError, _ as addSetSelectedDriveOnPopStateEventHandler, g as addSelectedDriveIdEventHandler, i as setVetraPackageManager, q as UnsupportedDocumentTypeError, r as addVetraPackageManagerEventHandler, w as addSelectedNodeIdEventHandler, x as useSelectedDriveId } from "./document-model-modules-DMIR-uSD.js";
|
|
3
|
+
import { t as makePHEventFunctions } from "./make-ph-event-functions-DBq3iWYn.js";
|
|
4
|
+
import { a as PhDocumentFieldsFragmentDoc, n as DocumentChangeType, o as PropagationMode, r as DocumentChangesDocument, t as createClient$1 } from "./client-D66jVnot.js";
|
|
5
|
+
import { f as addLoadingEventHandler, t as addRenownEventHandler } from "./renown-BHxwP7xU.js";
|
|
6
|
+
import { logger } from "document-model";
|
|
7
|
+
import { UnsupportedDocumentModelVersionError, assertAuthPreservedOnDuplicate, baseLoadFromInput, baseLoadFromInputVersioned, baseSaveToFileHandle, createPresignedHeader, createZip, deserializeSignature, documentModelDocumentType, generateId, hashDocumentStateForScope, normalizeDocumentModelVersion, replayDocumentVersioned, setName, setPreferredEditor, toTransportAction } from "@powerhousedao/shared/document-model";
|
|
8
|
+
import { addFolder, copyNode, generateNodesCopy, handleTargetNameCollisions, isFileNode, isFolderNode, moveNode, updateNode } from "@powerhousedao/shared/document-drive";
|
|
9
|
+
import { allPass, conditional, constant, forEachObj, funnel, isDefined, isNot, isStrictEqual, isString, isTruthy, once } from "remeda";
|
|
10
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
11
|
+
import { GraphQLClient } from "graphql-request";
|
|
12
|
+
import { gql } from "graphql-tag";
|
|
13
|
+
import { Kind, print } from "graphql";
|
|
14
|
+
import { createClient } from "graphql-ws/client";
|
|
15
|
+
import { Fragment, jsx } from "react/jsx-runtime";
|
|
16
|
+
//#region src/hooks/modals.ts
|
|
17
|
+
const modalEventFunctions = makePHEventFunctions("modal");
|
|
18
|
+
/** Returns the current modal */
|
|
19
|
+
const usePHModal = modalEventFunctions.useValue;
|
|
20
|
+
/** Sets the current modal */
|
|
21
|
+
const setPHModal = modalEventFunctions.setValue;
|
|
22
|
+
/** Adds an event handler for the modal */
|
|
23
|
+
const addModalEventHandler = modalEventFunctions.addEventHandler;
|
|
24
|
+
/** Shows a modal */
|
|
25
|
+
function showPHModal(modal) {
|
|
26
|
+
setPHModal(modal);
|
|
27
|
+
}
|
|
28
|
+
/** Closes the current modal */
|
|
29
|
+
function closePHModal() {
|
|
30
|
+
setPHModal(void 0);
|
|
31
|
+
}
|
|
32
|
+
/** Shows the create document modal */
|
|
33
|
+
function showCreateDocumentModal(documentType, documentModelVersion) {
|
|
34
|
+
setPHModal({
|
|
35
|
+
type: "createDocument",
|
|
36
|
+
documentType,
|
|
37
|
+
...documentModelVersion !== void 0 && { documentModelVersion }
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
/** Shows the delete node modal */
|
|
41
|
+
function showDeleteNodeModal(nodeOrId) {
|
|
42
|
+
setPHModal({
|
|
43
|
+
type: "deleteItem",
|
|
44
|
+
id: typeof nodeOrId === "string" ? nodeOrId : nodeOrId.id
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
//#endregion
|
|
48
|
+
//#region src/utils/documents.ts
|
|
49
|
+
function isDocumentTypeSupported(documentType, supportedDocuments) {
|
|
50
|
+
if (!supportedDocuments?.length) return true;
|
|
51
|
+
return supportedDocuments.some((pattern) => {
|
|
52
|
+
if (pattern.endsWith("/*")) {
|
|
53
|
+
const prefix = pattern.slice(0, -2);
|
|
54
|
+
return documentType.startsWith(prefix + "/");
|
|
55
|
+
}
|
|
56
|
+
if (pattern.endsWith("*") && !pattern.endsWith("/*")) {
|
|
57
|
+
const prefix = pattern.slice(0, -1);
|
|
58
|
+
return documentType.startsWith(prefix);
|
|
59
|
+
}
|
|
60
|
+
return pattern === documentType;
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
//#endregion
|
|
64
|
+
//#region src/utils/user.ts
|
|
65
|
+
function getUserPermissions() {
|
|
66
|
+
const user = window.ph?.renown?.user;
|
|
67
|
+
const allowList = window.ph?.allowList;
|
|
68
|
+
if (!allowList) return {
|
|
69
|
+
isAllowedToCreateDocuments: true,
|
|
70
|
+
isAllowedToEditDocuments: true
|
|
71
|
+
};
|
|
72
|
+
return {
|
|
73
|
+
isAllowedToCreateDocuments: allowList.includes(user?.address ?? ""),
|
|
74
|
+
isAllowedToEditDocuments: allowList.includes(user?.address ?? "")
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
//#endregion
|
|
78
|
+
//#region src/actions/document.ts
|
|
79
|
+
const NON_DOMAIN_SCOPES = new Set(["auth", "document"]);
|
|
80
|
+
/** An unreadable drive cannot be shown to hold no duplicate, so reads throw. */
|
|
81
|
+
async function isDocumentInLocation(reactorClient, document, driveId, parentFolder) {
|
|
82
|
+
const drive = await reactorClient.get(driveId);
|
|
83
|
+
const nodeById = drive.state.global.nodes.find((node) => node.id === document.header.id);
|
|
84
|
+
if (nodeById && nodeById.parentFolder === (parentFolder ?? null)) return {
|
|
85
|
+
isDuplicate: true,
|
|
86
|
+
duplicateType: "id",
|
|
87
|
+
nodeId: nodeById.id
|
|
88
|
+
};
|
|
89
|
+
const nodeByNameAndType = drive.state.global.nodes.find((node) => isFileNode(node) && node.name === document.header.name && node.documentType === document.header.documentType && node.parentFolder === (parentFolder ?? null));
|
|
90
|
+
if (nodeByNameAndType) return {
|
|
91
|
+
isDuplicate: true,
|
|
92
|
+
duplicateType: "name",
|
|
93
|
+
nodeId: nodeByNameAndType.id
|
|
94
|
+
};
|
|
95
|
+
return { isDuplicate: false };
|
|
96
|
+
}
|
|
97
|
+
function getDocumentTypeIcon(document) {
|
|
98
|
+
switch (document.header.documentType) {
|
|
99
|
+
case "powerhouse/document-model": return "document-model";
|
|
100
|
+
case "powerhouse/app": return "app";
|
|
101
|
+
case "powerhouse/document-editor": return "editor";
|
|
102
|
+
case "powerhouse/subgraph": return "subgraph";
|
|
103
|
+
case "powerhouse/package": return "package";
|
|
104
|
+
case "powerhouse/processor": {
|
|
105
|
+
const processorType = document.state.global?.type;
|
|
106
|
+
if (processorType === "analytics") return "analytics-processor";
|
|
107
|
+
if (processorType === "relational") return "relational-processor";
|
|
108
|
+
if (processorType === "codegen") return "codegen-processor";
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
default: return;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
async function downloadFile(document, fileName) {
|
|
115
|
+
try {
|
|
116
|
+
const data = await createZip(document);
|
|
117
|
+
const blob = new Blob([new Uint8Array(data)], { type: "application/zip" });
|
|
118
|
+
const link = window.document.createElement("a");
|
|
119
|
+
link.style.display = "none";
|
|
120
|
+
link.href = URL.createObjectURL(blob);
|
|
121
|
+
link.download = fileName;
|
|
122
|
+
window.document.body.appendChild(link);
|
|
123
|
+
link.click();
|
|
124
|
+
window.document.body.removeChild(link);
|
|
125
|
+
} catch (e) {
|
|
126
|
+
logger.error(e instanceof Error ? e.message : String(e));
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
async function getDocumentExtension(document) {
|
|
130
|
+
const documentType = document.header.documentType;
|
|
131
|
+
let rawExtension;
|
|
132
|
+
if (documentType === documentModelDocumentType) rawExtension = document.state.global?.extension;
|
|
133
|
+
else {
|
|
134
|
+
const reactorClient = window.ph?.reactorClientModule?.client;
|
|
135
|
+
if (reactorClient) {
|
|
136
|
+
const { results: documentModelModules } = await reactorClient.getDocumentModelModules();
|
|
137
|
+
rawExtension = documentModelModules.find((m) => m.documentModel.global.id === documentType)?.utils.fileExtension;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return (rawExtension ?? "").replace(/^\.+|\.+$/g, "");
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Fetches all operations for a document using cursor-based pagination.
|
|
144
|
+
* The reactor client handles multi-scope cursors transparently via
|
|
145
|
+
* composite cursors, so all scopes are fetched in a single paginated stream.
|
|
146
|
+
*/
|
|
147
|
+
async function fetchDocumentOperations(reactorClient, document, pageSize = 100) {
|
|
148
|
+
const scopes = Object.keys(document.state);
|
|
149
|
+
const operations = {};
|
|
150
|
+
for (const scope of scopes) operations[scope] = [];
|
|
151
|
+
let cursor = "";
|
|
152
|
+
do {
|
|
153
|
+
const page = await reactorClient.getOperations(document.header.id, { scopes }, void 0, {
|
|
154
|
+
cursor,
|
|
155
|
+
limit: pageSize
|
|
156
|
+
});
|
|
157
|
+
for (const op of page.results) {
|
|
158
|
+
const scope = op.action.scope ?? "global";
|
|
159
|
+
if (operations[scope]) operations[scope].push(op);
|
|
160
|
+
}
|
|
161
|
+
cursor = page.nextCursor ?? "";
|
|
162
|
+
} while (cursor);
|
|
163
|
+
return operations;
|
|
164
|
+
}
|
|
165
|
+
function extractInitialState(documentScopeOps) {
|
|
166
|
+
const upgradeOp = documentScopeOps.find((op) => op.action.type === "UPGRADE_DOCUMENT");
|
|
167
|
+
if (!upgradeOp) throw new Error("No UPGRADE_DOCUMENT operation found — document is invalid");
|
|
168
|
+
const input = upgradeOp.action.input;
|
|
169
|
+
const initialState = input.initialState ?? input.state;
|
|
170
|
+
if (!initialState) throw new Error("UPGRADE_DOCUMENT operation has no initialState — document is invalid");
|
|
171
|
+
return initialState;
|
|
172
|
+
}
|
|
173
|
+
function filterDomainOperations(operations) {
|
|
174
|
+
return Object.fromEntries(Object.entries(operations).filter(([scope]) => !NON_DOMAIN_SCOPES.has(scope)));
|
|
175
|
+
}
|
|
176
|
+
function countOperations(operations) {
|
|
177
|
+
let total = 0;
|
|
178
|
+
for (const ops of Object.values(operations)) total += ops?.length ?? 0;
|
|
179
|
+
return total;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Split an imported document's domain operations at its mid-history upgrade
|
|
183
|
+
* boundaries, so the caller can re-dispatch each upgrade between segments.
|
|
184
|
+
*
|
|
185
|
+
* The seed's CREATE_DOCUMENT and 0 -> N upgrade are replayed by the creation
|
|
186
|
+
* path and must not be repeated. A later upgrade must be: it is where the
|
|
187
|
+
* reducer version changes, and dropping it replays everything after it through
|
|
188
|
+
* the module the document was created with.
|
|
189
|
+
*/
|
|
190
|
+
function splitAtUpgradeBoundaries(operations) {
|
|
191
|
+
const upgrades = (operations.document ?? []).filter((op) => op.action.type === "UPGRADE_DOCUMENT").filter((op) => op.action.input.fromVersion > 0).sort((a, b) => a.index - b.index);
|
|
192
|
+
const domain = filterDomainOperations(operations);
|
|
193
|
+
if (upgrades.length === 0) return [{ operations: domain }];
|
|
194
|
+
const segments = [];
|
|
195
|
+
const consumed = /* @__PURE__ */ new Map();
|
|
196
|
+
const take = (until) => {
|
|
197
|
+
const slice = {};
|
|
198
|
+
for (const [scope, ops] of Object.entries(domain)) {
|
|
199
|
+
if (!ops) continue;
|
|
200
|
+
const from = consumed.get(scope) ?? 0;
|
|
201
|
+
const stop = Math.max(from, until(scope, ops));
|
|
202
|
+
const chunk = ops.slice(from, stop);
|
|
203
|
+
if (chunk.length > 0) slice[scope] = chunk;
|
|
204
|
+
consumed.set(scope, stop);
|
|
205
|
+
}
|
|
206
|
+
return slice;
|
|
207
|
+
};
|
|
208
|
+
for (const upgrade of upgrades) {
|
|
209
|
+
const input = upgrade.action.input;
|
|
210
|
+
const slice = take((scope, ops) => boundary(upgrade, scope, ops));
|
|
211
|
+
segments.push({
|
|
212
|
+
operations: slice,
|
|
213
|
+
upgradeTo: input.toVersion
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
const tail = take((_scope, ops) => ops.length);
|
|
217
|
+
if (countOperations(tail) > 0) segments.push({ operations: tail });
|
|
218
|
+
return segments;
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* How many of a scope's operations precede an upgrade. Must stay identical to
|
|
222
|
+
* the boundary computation in `replayDocumentVersioned` — snapshots compare
|
|
223
|
+
* against operation indices, not array positions, and the timestamp fallback
|
|
224
|
+
* excludes the upgrade's own instant.
|
|
225
|
+
*/
|
|
226
|
+
function boundary(upgrade, scope, ops) {
|
|
227
|
+
const snapshot = upgrade.action.input.revision;
|
|
228
|
+
if (snapshot !== void 0) {
|
|
229
|
+
const revision = snapshot[scope] ?? 0;
|
|
230
|
+
let count = 0;
|
|
231
|
+
for (let i = 0; i < ops.length; i++) if (ops[i].index < revision) count = i + 1;
|
|
232
|
+
return count;
|
|
233
|
+
}
|
|
234
|
+
for (let i = 0; i < ops.length; i++) if (ops[i].timestampUtcMs >= upgrade.timestampUtcMs) return i;
|
|
235
|
+
return ops.length;
|
|
236
|
+
}
|
|
237
|
+
async function exportFile(document, suggestedName) {
|
|
238
|
+
const reactorClient = window.ph?.reactorClient;
|
|
239
|
+
if (!reactorClient) throw new Error("ReactorClient not initialized");
|
|
240
|
+
const operations = await fetchDocumentOperations(reactorClient, document);
|
|
241
|
+
const initialState = extractInitialState(operations["document"] ?? []);
|
|
242
|
+
const documentWithOps = {
|
|
243
|
+
...document,
|
|
244
|
+
operations,
|
|
245
|
+
initialState
|
|
246
|
+
};
|
|
247
|
+
const extension = await getDocumentExtension(documentWithOps);
|
|
248
|
+
const baseName = suggestedName || documentWithOps.header.name || "Untitled";
|
|
249
|
+
const name = extension ? `${baseName}.${extension}.phd` : `${baseName}.phd`;
|
|
250
|
+
if (!window.showSaveFilePicker) return await downloadFile(documentWithOps, name);
|
|
251
|
+
try {
|
|
252
|
+
const fileHandle = await window.showSaveFilePicker({ suggestedName: name });
|
|
253
|
+
await baseSaveToFileHandle(documentWithOps, fileHandle);
|
|
254
|
+
return fileHandle;
|
|
255
|
+
} catch (e) {
|
|
256
|
+
if (!(e instanceof DOMException && e.name === "AbortError")) throw e;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Loads a document file and replays it with version-aware reducers from the registry.
|
|
261
|
+
* Falls back to single-version legacy replay when no upgrade manifest is registered for
|
|
262
|
+
* the document type.
|
|
263
|
+
*/
|
|
264
|
+
async function loadFile(path) {
|
|
265
|
+
const baseDocument = await baseLoadFromInput(path, (state) => state, { checkHashes: true });
|
|
266
|
+
const reactorClient = window.ph?.reactorClientModule?.client;
|
|
267
|
+
if (!reactorClient) throw new Error("ReactorClient not initialized");
|
|
268
|
+
const documentType = baseDocument.header.documentType;
|
|
269
|
+
const { results: documentModelModules } = await reactorClient.getDocumentModelModules();
|
|
270
|
+
const modulesForType = documentModelModules.filter((module) => module.documentModel.global.id === documentType);
|
|
271
|
+
if (modulesForType.length === 0) throw new DocumentModelNotFoundError(documentType);
|
|
272
|
+
const reducers = {};
|
|
273
|
+
for (const module of modulesForType) reducers[module.version ?? 1] = module.reducer;
|
|
274
|
+
const registry = window.ph?.reactorClientModule?.reactorModule?.documentModelRegistry;
|
|
275
|
+
let upgradeManifest;
|
|
276
|
+
if (registry) try {
|
|
277
|
+
upgradeManifest = registry.getUpgradeManifest(documentType);
|
|
278
|
+
} catch {}
|
|
279
|
+
return baseLoadFromInputVersioned(path, {
|
|
280
|
+
reducers,
|
|
281
|
+
upgradeManifest
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
async function addDocument(driveId, name, documentType, parentFolder, document, id, preferredEditor, documentModelVersion) {
|
|
285
|
+
const { isAllowedToCreateDocuments } = getUserPermissions();
|
|
286
|
+
if (!isAllowedToCreateDocuments) throw new Error("User is not allowed to create documents");
|
|
287
|
+
const reactorClient = window.ph?.reactorClientModule?.client;
|
|
288
|
+
if (!reactorClient) throw new Error("ReactorClient not initialized");
|
|
289
|
+
let documentModelModule;
|
|
290
|
+
if (documentModelVersion !== void 0) {
|
|
291
|
+
const { results: documentModelModules } = await reactorClient.getDocumentModelModules();
|
|
292
|
+
const module = documentModelModules.find((m) => m.documentModel.global.id === documentType && (m.version ?? 1) === documentModelVersion);
|
|
293
|
+
if (!module) throw new Error(`Document model not found for type: ${documentType} with version: ${documentModelVersion}`);
|
|
294
|
+
documentModelModule = module;
|
|
295
|
+
} else documentModelModule = await reactorClient.getDocumentModelModule(documentType);
|
|
296
|
+
const newDocument = document ?? documentModelModule.utils.createDocument();
|
|
297
|
+
if (!document) newDocument.state.document.version = documentModelModule.version ?? 1;
|
|
298
|
+
newDocument.header.name = name;
|
|
299
|
+
if (preferredEditor) newDocument.header.meta = {
|
|
300
|
+
...newDocument.header.meta,
|
|
301
|
+
preferredEditor
|
|
302
|
+
};
|
|
303
|
+
let newDoc;
|
|
304
|
+
try {
|
|
305
|
+
newDoc = await reactorClient.drives.addFile(driveId, newDocument, parentFolder);
|
|
306
|
+
} catch (e) {
|
|
307
|
+
logger.error("Error adding document: @error", e);
|
|
308
|
+
throw new Error("There was an error adding document", { cause: e });
|
|
309
|
+
}
|
|
310
|
+
return {
|
|
311
|
+
id: newDoc.header.id,
|
|
312
|
+
name: newDoc.header.name,
|
|
313
|
+
documentType,
|
|
314
|
+
parentFolder: parentFolder ?? null,
|
|
315
|
+
kind: "file"
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* How many ids an import will try before giving up. The check before the create
|
|
320
|
+
* catches an id that is already taken; this bound covers the window between the
|
|
321
|
+
* two, where a create can still lose to a claim made in the meantime.
|
|
322
|
+
*/
|
|
323
|
+
const IMPORT_ID_ATTEMPTS = 3;
|
|
324
|
+
/** True when this error, or anything it wraps, is a taken document id. */
|
|
325
|
+
function isDocumentAlreadyExists(error) {
|
|
326
|
+
let current = error;
|
|
327
|
+
while (current instanceof Error) {
|
|
328
|
+
if (current.name === "DocumentAlreadyExistsError") return true;
|
|
329
|
+
current = current.cause;
|
|
330
|
+
}
|
|
331
|
+
return false;
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Creates the imported document under an id that is free, retrying on a
|
|
335
|
+
* collision.
|
|
336
|
+
*
|
|
337
|
+
* The check cannot stand on its own: it answers for the moment it ran, and the
|
|
338
|
+
* create that follows can still land on an id claimed in between. The store
|
|
339
|
+
* rejects that create as DocumentAlreadyExistsError, which is not retryable
|
|
340
|
+
* under the same id and is resolved by minting another one. A create that
|
|
341
|
+
* fails leaves the drive untouched, so a retry has nothing to clean up.
|
|
342
|
+
*/
|
|
343
|
+
async function createImportedDocument(reactor, document, driveId, name, parentFolder) {
|
|
344
|
+
let documentId = await reactor.isDocumentIdTaken(document.header.id) ? generateId() : document.header.id;
|
|
345
|
+
for (let attempt = 1;; attempt++) {
|
|
346
|
+
const header = createPresignedHeader(documentId, document.header.documentType);
|
|
347
|
+
header.lastModifiedAtUtcIso = document.header.createdAtUtcIso;
|
|
348
|
+
header.meta = document.header.meta;
|
|
349
|
+
header.name = name || document.header.name;
|
|
350
|
+
const initialDocument = {
|
|
351
|
+
...document,
|
|
352
|
+
header,
|
|
353
|
+
state: document.initialState,
|
|
354
|
+
operations: Object.keys(document.operations).reduce((acc, key) => {
|
|
355
|
+
acc[key] = [];
|
|
356
|
+
return acc;
|
|
357
|
+
}, {})
|
|
358
|
+
};
|
|
359
|
+
try {
|
|
360
|
+
const fileNode = await addDocument(driveId, name || document.header.name, document.header.documentType, parentFolder, initialDocument, documentId, document.header.meta?.preferredEditor);
|
|
361
|
+
if (!fileNode) throw new Error("There was an error adding file");
|
|
362
|
+
return {
|
|
363
|
+
documentId,
|
|
364
|
+
fileNode
|
|
365
|
+
};
|
|
366
|
+
} catch (error) {
|
|
367
|
+
if (!isDocumentAlreadyExists(error) || attempt === IMPORT_ID_ATTEMPTS) throw error;
|
|
368
|
+
logger.warn(`Document id ${documentId} was taken between the check and the create; importing under a new id`);
|
|
369
|
+
documentId = generateId();
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
async function addFileWithProgress(file, driveId, name, parentFolder, onProgress, documentTypes, resolveConflict) {
|
|
374
|
+
logger.verbose(`addFileWithProgress(drive: ${driveId}, name: ${name}, folder: ${parentFolder})`);
|
|
375
|
+
const reactor = window.ph?.reactorClientModule?.client;
|
|
376
|
+
if (!reactor) {
|
|
377
|
+
onProgress?.({
|
|
378
|
+
stage: "failed",
|
|
379
|
+
progress: 100,
|
|
380
|
+
error: "ReactorClient not initialized"
|
|
381
|
+
});
|
|
382
|
+
throw new Error("ReactorClient not initialized");
|
|
383
|
+
}
|
|
384
|
+
const { isAllowedToCreateDocuments } = getUserPermissions();
|
|
385
|
+
if (!isAllowedToCreateDocuments) throw new Error("User is not allowed to create files");
|
|
386
|
+
try {
|
|
387
|
+
onProgress?.({
|
|
388
|
+
stage: "loading",
|
|
389
|
+
progress: 0
|
|
390
|
+
});
|
|
391
|
+
let document;
|
|
392
|
+
try {
|
|
393
|
+
document = await loadFile(file);
|
|
394
|
+
} catch (loadError) {
|
|
395
|
+
if (UnsupportedDocumentModelVersionError.isError(loadError)) {
|
|
396
|
+
showPHModal({
|
|
397
|
+
type: "documentVersionUnsupported",
|
|
398
|
+
documentType: loadError.documentType,
|
|
399
|
+
requiredVersion: loadError.requiredVersion,
|
|
400
|
+
availableVersions: loadError.availableVersions
|
|
401
|
+
});
|
|
402
|
+
onProgress?.({
|
|
403
|
+
stage: "failed",
|
|
404
|
+
progress: 100,
|
|
405
|
+
error: loadError.message
|
|
406
|
+
});
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
const discoveryService = window.ph?.packageDiscoveryService;
|
|
410
|
+
if (discoveryService && DocumentModelNotFoundError.isError(loadError)) {
|
|
411
|
+
retryAfterDiscovery(discoveryService, loadError.documentType, file, driveId, name, parentFolder, onProgress, documentTypes, resolveConflict).catch((retryError) => {
|
|
412
|
+
logger.error("Import retry after discovery failed: @error", retryError);
|
|
413
|
+
onProgress?.({
|
|
414
|
+
stage: "failed",
|
|
415
|
+
progress: 100,
|
|
416
|
+
error: retryError instanceof Error ? retryError.message : String(retryError)
|
|
417
|
+
});
|
|
418
|
+
});
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
throw loadError;
|
|
422
|
+
}
|
|
423
|
+
const duplicateCheck = await isDocumentInLocation(reactor, document, driveId, parentFolder);
|
|
424
|
+
if (duplicateCheck.isDuplicate && !resolveConflict) {
|
|
425
|
+
onProgress?.({
|
|
426
|
+
stage: "conflict",
|
|
427
|
+
progress: 0,
|
|
428
|
+
duplicateType: duplicateCheck.duplicateType
|
|
429
|
+
});
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
const documentType = getDocumentTypeIcon(document);
|
|
433
|
+
if (documentType) onProgress?.({
|
|
434
|
+
stage: "loading",
|
|
435
|
+
progress: 10,
|
|
436
|
+
documentType
|
|
437
|
+
});
|
|
438
|
+
else onProgress?.({
|
|
439
|
+
stage: "loading",
|
|
440
|
+
progress: 10
|
|
441
|
+
});
|
|
442
|
+
if (!isDocumentTypeSupported(document.header.documentType, documentTypes)) {
|
|
443
|
+
onProgress?.({
|
|
444
|
+
stage: "unsupported-document-type",
|
|
445
|
+
progress: 100,
|
|
446
|
+
error: `Document type ${document.header.documentType} is not supported`
|
|
447
|
+
});
|
|
448
|
+
throw new UnsupportedDocumentTypeError(document.header.documentType);
|
|
449
|
+
}
|
|
450
|
+
await reactor.getDocumentModelModule(document.header.documentType);
|
|
451
|
+
onProgress?.({
|
|
452
|
+
stage: "initializing",
|
|
453
|
+
progress: 10
|
|
454
|
+
});
|
|
455
|
+
const created = await createImportedDocument(reactor, document, driveId, name, parentFolder);
|
|
456
|
+
const documentId = created.documentId;
|
|
457
|
+
const fileNode = created.fileNode;
|
|
458
|
+
onProgress?.({
|
|
459
|
+
stage: "initializing",
|
|
460
|
+
progress: 20
|
|
461
|
+
});
|
|
462
|
+
await reactor.get(documentId);
|
|
463
|
+
console.log("Document created, starting upload of operations");
|
|
464
|
+
const segments = splitAtUpgradeBoundaries(document.operations);
|
|
465
|
+
const totalOperations = segments.reduce((total, segment) => total + countOperations(segment.operations), 0);
|
|
466
|
+
let uploadedBefore = 0;
|
|
467
|
+
for (const segment of segments) {
|
|
468
|
+
await uploadOperations(documentId, segment.operations, queueOperations, { onProgress: (uploadProgress) => {
|
|
469
|
+
if (uploadProgress.uploadedOperations === void 0) return;
|
|
470
|
+
const uploaded = uploadedBefore + uploadProgress.uploadedOperations;
|
|
471
|
+
const uploadPercent = totalOperations > 0 ? uploaded / totalOperations : 0;
|
|
472
|
+
onProgress?.({
|
|
473
|
+
stage: "uploading",
|
|
474
|
+
progress: 20 + Math.round(uploadPercent * 80),
|
|
475
|
+
totalOperations,
|
|
476
|
+
uploadedOperations: uploaded
|
|
477
|
+
});
|
|
478
|
+
} });
|
|
479
|
+
uploadedBefore += countOperations(segment.operations);
|
|
480
|
+
if (segment.upgradeTo !== void 0) await reactor.upgradeDocument(documentId, segment.upgradeTo);
|
|
481
|
+
}
|
|
482
|
+
onProgress?.({
|
|
483
|
+
stage: "complete",
|
|
484
|
+
progress: 100,
|
|
485
|
+
fileNode
|
|
486
|
+
});
|
|
487
|
+
return fileNode;
|
|
488
|
+
} catch (error) {
|
|
489
|
+
if (!UnsupportedDocumentTypeError.isError(error)) {
|
|
490
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
|
491
|
+
onProgress?.({
|
|
492
|
+
stage: "failed",
|
|
493
|
+
progress: 100,
|
|
494
|
+
error: errorMessage
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
throw error;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
async function retryAfterDiscovery(discoveryService, documentType, file, driveId, name, parentFolder, onProgress, documentTypes, resolveConflict) {
|
|
501
|
+
if (!discoveryService) return;
|
|
502
|
+
try {
|
|
503
|
+
await discoveryService.load(documentType);
|
|
504
|
+
} catch {
|
|
505
|
+
onProgress?.({
|
|
506
|
+
stage: "unsupported-document-type",
|
|
507
|
+
progress: 100,
|
|
508
|
+
error: `Document type ${documentType} is not supported`
|
|
509
|
+
});
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
await addFileWithProgress(file, driveId, name, parentFolder, onProgress, documentTypes, resolveConflict);
|
|
513
|
+
}
|
|
514
|
+
async function addFolder$1(driveId, name, parentFolder) {
|
|
515
|
+
const { isAllowedToCreateDocuments } = getUserPermissions();
|
|
516
|
+
if (!isAllowedToCreateDocuments) throw new Error("User is not allowed to create folders");
|
|
517
|
+
const reactorClient = window.ph?.reactorClient;
|
|
518
|
+
if (!reactorClient) throw new Error("ReactorClient not initialized");
|
|
519
|
+
await reactorClient.get(driveId);
|
|
520
|
+
const folderId = generateId();
|
|
521
|
+
const node = (await reactorClient.execute(driveId, "main", [addFolder({
|
|
522
|
+
id: folderId,
|
|
523
|
+
name,
|
|
524
|
+
parentFolder
|
|
525
|
+
})])).state.global.nodes.find((node) => node.id === folderId);
|
|
526
|
+
if (!node || !isFolderNode(node)) throw new Error("There was an error adding folder");
|
|
527
|
+
return node;
|
|
528
|
+
}
|
|
529
|
+
async function deleteNode(driveId, nodeId) {
|
|
530
|
+
const { isAllowedToCreateDocuments } = getUserPermissions();
|
|
531
|
+
if (!isAllowedToCreateDocuments) throw new Error("User is not allowed to delete documents");
|
|
532
|
+
const reactorClient = window.ph?.reactorClientModule?.client;
|
|
533
|
+
if (!reactorClient) throw new Error("ReactorClient not initialized");
|
|
534
|
+
await reactorClient.drives.removeNode(driveId, nodeId);
|
|
535
|
+
}
|
|
536
|
+
async function renameNode(driveId, nodeId, name) {
|
|
537
|
+
const { isAllowedToCreateDocuments } = getUserPermissions();
|
|
538
|
+
if (!isAllowedToCreateDocuments) throw new Error("User is not allowed to rename documents");
|
|
539
|
+
const reactorClient = window.ph?.reactorClient;
|
|
540
|
+
if (!reactorClient) throw new Error("ReactorClient not initialized");
|
|
541
|
+
if ((await reactorClient.execute(nodeId, "main", [setName({ name })])).header.name !== name) throw new Error("There was an error renaming the node");
|
|
542
|
+
const node = (await reactorClient.execute(driveId, "main", [updateNode({
|
|
543
|
+
id: nodeId,
|
|
544
|
+
name
|
|
545
|
+
})])).state.global.nodes.find((n) => n.id === nodeId);
|
|
546
|
+
if (!node) throw new Error("There was an error renaming node in the drive");
|
|
547
|
+
return node;
|
|
548
|
+
}
|
|
549
|
+
async function renameDriveNode(driveId, nodeId, name) {
|
|
550
|
+
const { isAllowedToCreateDocuments } = getUserPermissions();
|
|
551
|
+
if (!isAllowedToCreateDocuments) throw new Error("User is not allowed to rename documents");
|
|
552
|
+
const reactorClient = window.ph?.reactorClient;
|
|
553
|
+
if (!reactorClient) throw new Error("ReactorClient not initialized");
|
|
554
|
+
if ((await reactorClient.execute(nodeId, "main", [setName({ name })])).header.name !== name) throw new Error("There was an error renaming the node");
|
|
555
|
+
await reactorClient.execute(driveId, "main", [updateNode({
|
|
556
|
+
id: nodeId,
|
|
557
|
+
name
|
|
558
|
+
})]);
|
|
559
|
+
return (await reactorClient.get(driveId)).state.global.nodes.find((n) => n.id === nodeId);
|
|
560
|
+
}
|
|
561
|
+
async function setPreferredEditorOnNode(nodeId, preferredEditor) {
|
|
562
|
+
const { isAllowedToCreateDocuments } = getUserPermissions();
|
|
563
|
+
if (!isAllowedToCreateDocuments) throw new Error("User is not allowed to modify documents");
|
|
564
|
+
const reactorClient = window.ph?.reactorClient;
|
|
565
|
+
if (!reactorClient) throw new Error("ReactorClient not initialized");
|
|
566
|
+
return reactorClient.execute(nodeId, "main", [setPreferredEditor(preferredEditor)]);
|
|
567
|
+
}
|
|
568
|
+
async function moveNodeById(args) {
|
|
569
|
+
const { driveId, srcId, targetId } = args;
|
|
570
|
+
if (!driveId || !srcId) return;
|
|
571
|
+
const { isAllowedToCreateDocuments } = getUserPermissions();
|
|
572
|
+
if (!isAllowedToCreateDocuments) throw new Error("User is not allowed to move documents");
|
|
573
|
+
const reactorClient = window.ph?.reactorClient;
|
|
574
|
+
if (!reactorClient) throw new Error("ReactorClient not initialized");
|
|
575
|
+
const targetParentFolder = conditional(targetId, [isNot(isDefined), constant(void 0)], [isStrictEqual(driveId), constant(void 0)], constant(targetId));
|
|
576
|
+
if (isStrictEqual(targetParentFolder, srcId)) return;
|
|
577
|
+
return await reactorClient.execute(driveId, "main", [moveNode({
|
|
578
|
+
srcFolder: srcId,
|
|
579
|
+
targetParentFolder
|
|
580
|
+
})]);
|
|
581
|
+
}
|
|
582
|
+
async function moveNode$1(driveId, src, target) {
|
|
583
|
+
const { isAllowedToCreateDocuments } = getUserPermissions();
|
|
584
|
+
if (!isAllowedToCreateDocuments) throw new Error("User is not allowed to move documents");
|
|
585
|
+
const reactorClient = window.ph?.reactorClient;
|
|
586
|
+
if (!reactorClient) throw new Error("ReactorClient not initialized");
|
|
587
|
+
return await reactorClient.execute(driveId, "main", [moveNode({
|
|
588
|
+
srcFolder: src.id,
|
|
589
|
+
targetParentFolder: target?.id
|
|
590
|
+
})]);
|
|
591
|
+
}
|
|
592
|
+
/**
|
|
593
|
+
* Duplicates a document under a new id using version-aware replay.
|
|
594
|
+
* Falls back gracefully when no upgrade manifest is registered for the document type.
|
|
595
|
+
*/
|
|
596
|
+
async function _duplicateDocument(reactor, document, newId = generateId()) {
|
|
597
|
+
const documentType = document.header.documentType;
|
|
598
|
+
const { results: allModules } = await reactor.getDocumentModelModules();
|
|
599
|
+
const modulesForType = allModules.filter((m) => m.documentModel.global.id === documentType);
|
|
600
|
+
const reducers = {};
|
|
601
|
+
for (const m of modulesForType) reducers[m.version ?? 1] = m.reducer;
|
|
602
|
+
if (Object.keys(reducers).length === 0) throw new Error(`Document model module not found for type: ${documentType}`);
|
|
603
|
+
const registry = window.ph?.reactorClientModule?.reactorModule?.documentModelRegistry;
|
|
604
|
+
let upgradeManifest;
|
|
605
|
+
if (registry) try {
|
|
606
|
+
upgradeManifest = registry.getUpgradeManifest(documentType);
|
|
607
|
+
} catch {}
|
|
608
|
+
const config = {
|
|
609
|
+
reducers,
|
|
610
|
+
upgradeManifest
|
|
611
|
+
};
|
|
612
|
+
const header = createPresignedHeader(newId, documentType);
|
|
613
|
+
const duplicated = replayDocumentVersioned(document.initialState, document.operations, config, header);
|
|
614
|
+
assertAuthPreservedOnDuplicate(document.header.id, document.state.auth, duplicated.state.auth);
|
|
615
|
+
return duplicated;
|
|
616
|
+
}
|
|
617
|
+
async function copyNode$1(driveId, src, target) {
|
|
618
|
+
const reactor = window.ph?.reactorClientModule?.client;
|
|
619
|
+
if (!reactor) return;
|
|
620
|
+
const { isAllowedToCreateDocuments } = getUserPermissions();
|
|
621
|
+
if (!isAllowedToCreateDocuments) throw new Error("User is not allowed to copy documents");
|
|
622
|
+
const drive = await reactor.get(driveId);
|
|
623
|
+
const copyNodesInput = generateNodesCopy({
|
|
624
|
+
srcId: src.id,
|
|
625
|
+
targetParentFolder: target?.id,
|
|
626
|
+
targetName: src.name
|
|
627
|
+
}, () => generateId(), drive.state.global.nodes);
|
|
628
|
+
const resolvedNamesMap = /* @__PURE__ */ new Map();
|
|
629
|
+
for (const copyNodeInput of copyNodesInput) {
|
|
630
|
+
const node = drive.state.global.nodes.find((n) => n.id === copyNodeInput.srcId);
|
|
631
|
+
if (node) {
|
|
632
|
+
const resolvedName = handleTargetNameCollisions({
|
|
633
|
+
nodes: drive.state.global.nodes,
|
|
634
|
+
srcName: copyNodeInput.targetName || node.name,
|
|
635
|
+
srcKind: isFileNode(node) ? "file" : "folder",
|
|
636
|
+
targetParentFolder: copyNodeInput.targetParentFolder || null
|
|
637
|
+
});
|
|
638
|
+
resolvedNamesMap.set(copyNodeInput.targetId, resolvedName);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
const fileNodesToCopy = copyNodesInput.filter((copyNodeInput) => {
|
|
642
|
+
const node = drive.state.global.nodes.find((node) => node.id === copyNodeInput.srcId);
|
|
643
|
+
return node !== void 0 && isFileNode(node);
|
|
644
|
+
});
|
|
645
|
+
for (const fileNodeToCopy of fileNodesToCopy) try {
|
|
646
|
+
const duplicatedDocument = await _duplicateDocument(reactor, await reactor.get(fileNodeToCopy.srcId), fileNodeToCopy.targetId);
|
|
647
|
+
const resolvedName = resolvedNamesMap.get(fileNodeToCopy.targetId);
|
|
648
|
+
if (resolvedName) duplicatedDocument.header.name = resolvedName;
|
|
649
|
+
await reactor.drives.addFile(driveId, duplicatedDocument, target?.id);
|
|
650
|
+
} catch (e) {
|
|
651
|
+
logger.error(`Error copying document ${fileNodeToCopy.srcId}: ${String(e)}`);
|
|
652
|
+
}
|
|
653
|
+
return await queueActions(drive, copyNodesInput.map((copyNodeInput) => copyNode(copyNodeInput)));
|
|
654
|
+
}
|
|
655
|
+
/**
|
|
656
|
+
* Upgrades a document to a newer document model version. Defaults to the
|
|
657
|
+
* latest registered version for the document's type.
|
|
658
|
+
*/
|
|
659
|
+
async function upgradeDocument(documentId, toVersion) {
|
|
660
|
+
const reactorClient = window.ph?.reactorClientModule?.client;
|
|
661
|
+
if (!reactorClient) throw new Error("ReactorClient not initialized");
|
|
662
|
+
return reactorClient.upgradeDocument(documentId, toVersion);
|
|
663
|
+
}
|
|
664
|
+
//#endregion
|
|
665
|
+
//#region src/remote-controller/utils.ts
|
|
666
|
+
/** Convert SCREAMING_SNAKE_CASE to camelCase (e.g. "SET_MODEL_NAME" → "setModelName"). */
|
|
667
|
+
function screamingSnakeToCamel(s) {
|
|
668
|
+
return s.toLowerCase().replace(/_([a-z])/g, (_, c) => c.toUpperCase());
|
|
669
|
+
}
|
|
670
|
+
/** Error thrown when a push conflict is detected with the "reject" strategy. */
|
|
671
|
+
var ConflictError = class extends Error {
|
|
672
|
+
constructor(conflict) {
|
|
673
|
+
super("Push conflict: remote has new operations since last pull");
|
|
674
|
+
this.conflict = conflict;
|
|
675
|
+
this.name = "ConflictError";
|
|
676
|
+
}
|
|
677
|
+
};
|
|
678
|
+
/** Convert a remote operation to the local Operation type. */
|
|
679
|
+
function remoteOperationToLocal(remote) {
|
|
680
|
+
return {
|
|
681
|
+
id: remote.id ?? "",
|
|
682
|
+
index: remote.index,
|
|
683
|
+
skip: remote.skip,
|
|
684
|
+
timestampUtcMs: remote.timestampUtcMs,
|
|
685
|
+
hash: remote.hash,
|
|
686
|
+
error: remote.error ?? void 0,
|
|
687
|
+
deniedReason: remote.deniedReason ?? void 0,
|
|
688
|
+
action: {
|
|
689
|
+
id: remote.action.id,
|
|
690
|
+
type: remote.action.type,
|
|
691
|
+
timestampUtcMs: remote.action.timestampUtcMs,
|
|
692
|
+
input: remote.action.input,
|
|
693
|
+
scope: remote.action.scope,
|
|
694
|
+
context: remote.action.context?.signer ? { signer: {
|
|
695
|
+
user: remote.action.context.signer.user ?? {
|
|
696
|
+
address: "",
|
|
697
|
+
networkId: "",
|
|
698
|
+
chainId: 0
|
|
699
|
+
},
|
|
700
|
+
app: remote.action.context.signer.app ?? {
|
|
701
|
+
name: "",
|
|
702
|
+
key: ""
|
|
703
|
+
},
|
|
704
|
+
signatures: remote.action.context.signer.signatures.map((s) => deserializeSignature(s))
|
|
705
|
+
} } : void 0
|
|
706
|
+
}
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
/** Convert remote operations to local DocumentOperations format. */
|
|
710
|
+
function convertRemoteOperations(operationsByScope) {
|
|
711
|
+
const operations = {};
|
|
712
|
+
for (const [scope, remoteOps] of Object.entries(operationsByScope)) operations[scope] = remoteOps.map((op) => remoteOperationToLocal(op));
|
|
713
|
+
return operations;
|
|
714
|
+
}
|
|
715
|
+
/** Reconstruct a PHDocument from remote document data and operations. */
|
|
716
|
+
function buildPulledDocument(remoteDoc, operations, initialDoc, branch) {
|
|
717
|
+
return {
|
|
718
|
+
header: {
|
|
719
|
+
...initialDoc.header,
|
|
720
|
+
id: remoteDoc.id,
|
|
721
|
+
name: remoteDoc.name,
|
|
722
|
+
slug: remoteDoc.slug ?? "",
|
|
723
|
+
documentType: remoteDoc.documentType,
|
|
724
|
+
createdAtUtcIso: typeof remoteDoc.createdAtUtcIso === "string" ? remoteDoc.createdAtUtcIso : remoteDoc.createdAtUtcIso.toISOString(),
|
|
725
|
+
lastModifiedAtUtcIso: typeof remoteDoc.lastModifiedAtUtcIso === "string" ? remoteDoc.lastModifiedAtUtcIso : remoteDoc.lastModifiedAtUtcIso.toISOString(),
|
|
726
|
+
revision: Object.fromEntries(remoteDoc.revisionsList.map((r) => [r.scope, r.revision])),
|
|
727
|
+
branch
|
|
728
|
+
},
|
|
729
|
+
state: remoteDoc.state,
|
|
730
|
+
initialState: initialDoc.initialState,
|
|
731
|
+
operations,
|
|
732
|
+
clipboard: []
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
/** Extract revision map from a remote document's revisionsList. */
|
|
736
|
+
function extractRevisionMap(revisionsList) {
|
|
737
|
+
return Object.fromEntries(revisionsList.map((r) => [r.scope, r.revision]));
|
|
738
|
+
}
|
|
739
|
+
/**
|
|
740
|
+
* Check if any scope in currentRevision is ahead of knownRevision.
|
|
741
|
+
* When `scopes` is provided, only those scopes are checked.
|
|
742
|
+
*/
|
|
743
|
+
function hasRevisionConflict(currentRevision, knownRevision, scopes) {
|
|
744
|
+
for (const scope in currentRevision) {
|
|
745
|
+
if (scopes && !scopes.has(scope)) continue;
|
|
746
|
+
if ((currentRevision[scope] ?? 0) > (knownRevision[scope] ?? 0)) return true;
|
|
747
|
+
}
|
|
748
|
+
return false;
|
|
749
|
+
}
|
|
750
|
+
//#endregion
|
|
751
|
+
//#region src/graphql-client/adapter.ts
|
|
752
|
+
/**
|
|
753
|
+
* Converts a `GetDocument` result into a `PHDocument`.
|
|
754
|
+
*
|
|
755
|
+
* Unlike the legacy `src/graphql/adapters.ts` this preserves the per-scope
|
|
756
|
+
* revision map, which the write path needs to compute the `prevOpIndex` of a
|
|
757
|
+
* signed action. `sig` is filled with the empty shape used by
|
|
758
|
+
* `createPresignedHeader`: the creation signature is not fetchable over
|
|
759
|
+
* GraphQL and is not part of the operation signing payload.
|
|
760
|
+
*
|
|
761
|
+
* The read path never fetches operation history, so `operations` is a map of
|
|
762
|
+
* the document's known scopes to empty arrays.
|
|
763
|
+
*
|
|
764
|
+
* `branch` is the branch the document was read from. The GraphQL type does not
|
|
765
|
+
* carry it, so it has to be passed back in by the caller; it defaults to the
|
|
766
|
+
* server's own default branch.
|
|
767
|
+
*/
|
|
768
|
+
function phDocumentFromGetDocument(document, branch) {
|
|
769
|
+
const state = document.state;
|
|
770
|
+
return {
|
|
771
|
+
header: phDocumentHeaderFromGetDocument(document, branch),
|
|
772
|
+
state,
|
|
773
|
+
initialState: state,
|
|
774
|
+
operations: emptyOperationsForScopes(document.revisionsList),
|
|
775
|
+
clipboard: []
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
/**
|
|
779
|
+
* Converts a mutation result into a `PHDocument`, carrying the operations the
|
|
780
|
+
* mutation produced.
|
|
781
|
+
*
|
|
782
|
+
* `dispatchActions` reads per-action failures out of `operations[scope]`, so
|
|
783
|
+
* unlike the read path this one must not return empty operation lists.
|
|
784
|
+
*/
|
|
785
|
+
function phDocumentFromMutation(document, operations, branch) {
|
|
786
|
+
const phDocument = phDocumentFromGetDocument(document, branch);
|
|
787
|
+
const operationsByScope = { ...phDocument.operations };
|
|
788
|
+
for (const remote of operations) {
|
|
789
|
+
const operation = remoteOperationToLocal(remote);
|
|
790
|
+
const scope = operation.action.scope;
|
|
791
|
+
const scopeOperations = operationsByScope[scope] ?? [];
|
|
792
|
+
scopeOperations.push(operation);
|
|
793
|
+
operationsByScope[scope] = scopeOperations;
|
|
794
|
+
}
|
|
795
|
+
phDocument.operations = operationsByScope;
|
|
796
|
+
return phDocument;
|
|
797
|
+
}
|
|
798
|
+
/** Turns a `revisionsList` selection into the header's per-scope revision map. */
|
|
799
|
+
function revisionMapFromRevisionsList(revisionsList) {
|
|
800
|
+
return Object.fromEntries(revisionsList.map((r) => [r.scope, r.revision]));
|
|
801
|
+
}
|
|
802
|
+
/** The branch the server reads and writes when none is asked for. */
|
|
803
|
+
const defaultBranch = "main";
|
|
804
|
+
function phDocumentHeaderFromGetDocument(document, branch) {
|
|
805
|
+
return {
|
|
806
|
+
id: document.id,
|
|
807
|
+
sig: {
|
|
808
|
+
publicKey: {},
|
|
809
|
+
nonce: ""
|
|
810
|
+
},
|
|
811
|
+
documentType: document.documentType,
|
|
812
|
+
createdAtUtcIso: isoStringFromDateTime(document.createdAtUtcIso),
|
|
813
|
+
slug: document.slug ?? "",
|
|
814
|
+
name: document.name,
|
|
815
|
+
branch: branch ?? defaultBranch,
|
|
816
|
+
revision: revisionMapFromRevisionsList(document.revisionsList),
|
|
817
|
+
lastModifiedAtUtcIso: isoStringFromDateTime(document.lastModifiedAtUtcIso)
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
function emptyOperationsForScopes(revisionsList) {
|
|
821
|
+
return Object.fromEntries(revisionsList.map((r) => [r.scope, []]));
|
|
822
|
+
}
|
|
823
|
+
/** The `DateTime` scalar deserializes as either an ISO string or a `Date`. */
|
|
824
|
+
function isoStringFromDateTime(value) {
|
|
825
|
+
return value instanceof Date ? value.toISOString() : value;
|
|
826
|
+
}
|
|
827
|
+
//#endregion
|
|
828
|
+
//#region src/graphql-client/operations.ts
|
|
829
|
+
/**
|
|
830
|
+
* The operation selection of `GetDocumentOperations`, lifted into a fragment so
|
|
831
|
+
* the write path can reuse `remoteOperationToLocal` on the items it selects.
|
|
832
|
+
*/
|
|
833
|
+
const ReactorOperationFieldsFragmentDoc = gql`
|
|
834
|
+
fragment ReactorOperationFields on ReactorOperation {
|
|
835
|
+
index
|
|
836
|
+
timestampUtcMs
|
|
837
|
+
hash
|
|
838
|
+
skip
|
|
839
|
+
error
|
|
840
|
+
deniedReason
|
|
841
|
+
id
|
|
842
|
+
action {
|
|
843
|
+
id
|
|
844
|
+
type
|
|
845
|
+
timestampUtcMs
|
|
846
|
+
input
|
|
847
|
+
scope
|
|
848
|
+
context {
|
|
849
|
+
signer {
|
|
850
|
+
user {
|
|
851
|
+
address
|
|
852
|
+
networkId
|
|
853
|
+
chainId
|
|
854
|
+
}
|
|
855
|
+
app {
|
|
856
|
+
name
|
|
857
|
+
key
|
|
858
|
+
}
|
|
859
|
+
signatures
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
`;
|
|
865
|
+
/**
|
|
866
|
+
* `mutateDocument` plus the operations the mutation produced.
|
|
867
|
+
*
|
|
868
|
+
* The generated `MutateDocument` only selects the document fields, but
|
|
869
|
+
* `dispatchActions` reads per-action failures out of `result.operations[scope]`
|
|
870
|
+
* (`src/actions/dispatch.ts`), so the reducer-level errors would be lost.
|
|
871
|
+
*
|
|
872
|
+
* The operations filter keeps the extra selection to the operations this call
|
|
873
|
+
* could have appended rather than the whole history. All three of its fields
|
|
874
|
+
* matter:
|
|
875
|
+
* - `sinceRevision` is the lowest head revision across the targeted scopes, so
|
|
876
|
+
* it is only a safe window when the selection is scope-limited as well;
|
|
877
|
+
* - `scopes` limits the response to the scopes the pushed actions target,
|
|
878
|
+
* without it the server walks every scope of the document
|
|
879
|
+
* (`packages/reactor/src/core/reactor.ts` `getOperations`);
|
|
880
|
+
* - `branch` selects the branch the operations are read from, and is the same
|
|
881
|
+
* variable the mutation writes to. It used to be two - the write went through
|
|
882
|
+
* a `view`, the filter took its own `branch` - and they had to be kept equal
|
|
883
|
+
* by hand or the operations came from `main` while the actions were applied
|
|
884
|
+
* elsewhere. One variable cannot disagree with itself.
|
|
885
|
+
*/
|
|
886
|
+
const MutateDocumentWithOperationsDocument = gql`
|
|
887
|
+
mutation MutateDocumentWithOperations(
|
|
888
|
+
$documentIdentifier: String!
|
|
889
|
+
$actions: [ActionInput!]!
|
|
890
|
+
$sinceRevision: Int
|
|
891
|
+
$scopes: [String!]
|
|
892
|
+
$branch: String
|
|
893
|
+
) {
|
|
894
|
+
mutateDocument: execute(
|
|
895
|
+
documentIdentifier: $documentIdentifier
|
|
896
|
+
actions: $actions
|
|
897
|
+
branch: $branch
|
|
898
|
+
) {
|
|
899
|
+
...PHDocumentFields
|
|
900
|
+
operations(
|
|
901
|
+
filter: {
|
|
902
|
+
sinceRevision: $sinceRevision
|
|
903
|
+
scopes: $scopes
|
|
904
|
+
branch: $branch
|
|
905
|
+
}
|
|
906
|
+
) {
|
|
907
|
+
items {
|
|
908
|
+
...ReactorOperationFields
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
${PhDocumentFieldsFragmentDoc}
|
|
914
|
+
${ReactorOperationFieldsFragmentDoc}
|
|
915
|
+
`;
|
|
916
|
+
//#endregion
|
|
917
|
+
//#region src/graphql-client/signing.ts
|
|
918
|
+
/**
|
|
919
|
+
* Stamps an action with the head of the scope it targets.
|
|
920
|
+
*
|
|
921
|
+
* `prevOpHash` is the hash of the scope state the action is meant to apply to;
|
|
922
|
+
* for a document at head that is the hash of the last operation in the scope,
|
|
923
|
+
* which the read path cannot fetch, so it is recomputed from the state.
|
|
924
|
+
* `prevOpIndex` is the index of that last operation: revisions count
|
|
925
|
+
* operations and indices are zero based, so an empty scope stamps `-1`, which
|
|
926
|
+
* matches what the remote controller records.
|
|
927
|
+
*
|
|
928
|
+
* `revision` overrides the scope revision the index is derived from. Only a
|
|
929
|
+
* batch needs it - {@link prepareSignedActions} stamps action N at the revision
|
|
930
|
+
* the N actions before it will have produced, which the document itself does
|
|
931
|
+
* not know yet. `prevOpHash` always comes from the supplied document's state.
|
|
932
|
+
*/
|
|
933
|
+
function stampAction(action, document, revision = document.header.revision[action.scope] ?? 0) {
|
|
934
|
+
return {
|
|
935
|
+
...action,
|
|
936
|
+
context: {
|
|
937
|
+
...action.context,
|
|
938
|
+
prevOpHash: hashDocumentStateForScope(document, action.scope),
|
|
939
|
+
prevOpIndex: revision - 1
|
|
940
|
+
}
|
|
941
|
+
};
|
|
942
|
+
}
|
|
943
|
+
/**
|
|
944
|
+
* Signs a stamped action, preserving any signatures it already carries.
|
|
945
|
+
*
|
|
946
|
+
* The signer reads `context.prevOpHash` off the action, so this must run after
|
|
947
|
+
* {@link stampAction}.
|
|
948
|
+
*/
|
|
949
|
+
async function signStampedAction(action, signer, signal) {
|
|
950
|
+
const actionSigner = action.context?.signer;
|
|
951
|
+
const user = actionSigner?.user ?? signer.user;
|
|
952
|
+
const app = actionSigner?.app ?? signer.app;
|
|
953
|
+
if (!user || !app) throw new Error("cannot sign an action: the signer has no user or app identity");
|
|
954
|
+
const signature = await signer.signAction(action, signal);
|
|
955
|
+
return {
|
|
956
|
+
...action,
|
|
957
|
+
context: {
|
|
958
|
+
...action.context,
|
|
959
|
+
signer: {
|
|
960
|
+
user,
|
|
961
|
+
app,
|
|
962
|
+
signatures: [...actionSigner?.signatures ?? [], signature]
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
};
|
|
966
|
+
}
|
|
967
|
+
/**
|
|
968
|
+
* Actions a snapshot batch cannot predict the next state for.
|
|
969
|
+
*
|
|
970
|
+
* `UNDO`, `REDO` and `PRUNE` rewrite history the light read path never fetched
|
|
971
|
+
* (and the reducer appends no operation for them); `NOOP` is only ever produced
|
|
972
|
+
* by an undo chain; the document-scope actions run through the reactor's
|
|
973
|
+
* document-action handler rather than a document-model reducer
|
|
974
|
+
* (`DOCUMENT_SCOPE_ACTIONS` in packages/reactor/src/executor/util.ts) and
|
|
975
|
+
* `UPGRADE_DOCUMENT` changes which reducer applies mid-batch.
|
|
976
|
+
*/
|
|
977
|
+
const unsupportedBatchActions = new Set([
|
|
978
|
+
"UNDO",
|
|
979
|
+
"REDO",
|
|
980
|
+
"PRUNE",
|
|
981
|
+
"NOOP",
|
|
982
|
+
"CREATE_DOCUMENT",
|
|
983
|
+
"DELETE_DOCUMENT",
|
|
984
|
+
"UPGRADE_DOCUMENT",
|
|
985
|
+
"ADD_RELATIONSHIP",
|
|
986
|
+
"REMOVE_RELATIONSHIP",
|
|
987
|
+
"UPDATE_RELATIONSHIP"
|
|
988
|
+
]);
|
|
989
|
+
/**
|
|
990
|
+
* The base-reducer protocol version to predict with when the snapshot's header
|
|
991
|
+
* carries none.
|
|
992
|
+
*
|
|
993
|
+
* `baseReducer` reads `baseReducerVersion(document.header)` for every action and
|
|
994
|
+
* throws when the header has no `protocolVersions` - and a header built by
|
|
995
|
+
* {@link phDocumentFromGetDocument} never has one, because the GraphQL
|
|
996
|
+
* `PHDocument` type does not expose it. The version only selects between the v1
|
|
997
|
+
* and v2 UNDO/NOOP branches, and every action reaching the prediction loop is an
|
|
998
|
+
* ordinary append-only one, so it cannot change the predicted state here; it is
|
|
999
|
+
* passed purely so the lookup does not throw.
|
|
1000
|
+
*/
|
|
1001
|
+
const predictionProtocolVersion = 2;
|
|
1002
|
+
/**
|
|
1003
|
+
* Stamps and signs a batch of actions against one document snapshot, so every
|
|
1004
|
+
* action carries the head its predecessor will leave behind.
|
|
1005
|
+
*
|
|
1006
|
+
* The server executes the array in order, each action against the state the one
|
|
1007
|
+
* before it produced. The snapshot the light client reads has correct state and
|
|
1008
|
+
* per-scope revisions but NO operation history (see `adapter.ts`), so the chain
|
|
1009
|
+
* is predicted here: reduce action N over the working document to get the state
|
|
1010
|
+
* action N+1 hashes, and carry a virtual revision alongside it. The revision has
|
|
1011
|
+
* to be tracked separately because the reducer derives operation indices from
|
|
1012
|
+
* the last stored operation, and with an empty history that counts from zero
|
|
1013
|
+
* rather than from the document's real revision.
|
|
1014
|
+
*
|
|
1015
|
+
* A single action needs no prediction and therefore no `module`, which keeps
|
|
1016
|
+
* today's callers working unchanged. Two or more require the document's exact
|
|
1017
|
+
* reducer, one shared scope (the reactor rejects mixed-scope jobs) and only
|
|
1018
|
+
* append-only actions. Anything else rejects: a batch that cannot be predicted
|
|
1019
|
+
* must fail before the mutation, never be downgraded to unsigned.
|
|
1020
|
+
*/
|
|
1021
|
+
async function prepareSignedActions(actions, snapshot, signer, module, signal) {
|
|
1022
|
+
if (actions.length === 0) return [];
|
|
1023
|
+
if (actions.length === 1) return [await signStampedAction(stampAction(actions[0], snapshot), signer, signal)];
|
|
1024
|
+
const scope = sharedScope(actions);
|
|
1025
|
+
assertSupportedBatch(actions);
|
|
1026
|
+
if (!module) throw new Error(`cannot sign ${actions.length} actions: no document model module for ${snapshot.header.documentType} was supplied, so the batch state cannot be predicted`);
|
|
1027
|
+
let working = {
|
|
1028
|
+
...snapshot,
|
|
1029
|
+
operations: {
|
|
1030
|
+
...snapshot.operations,
|
|
1031
|
+
[scope]: [...snapshot.operations[scope] ?? []]
|
|
1032
|
+
}
|
|
1033
|
+
};
|
|
1034
|
+
let revision = snapshot.header.revision[scope] ?? 0;
|
|
1035
|
+
const signed = [];
|
|
1036
|
+
for (const [index, action] of actions.entries()) {
|
|
1037
|
+
throwIfAborted(signal, index);
|
|
1038
|
+
const signedAction = await signStampedAction(stampAction(action, working, revision), signer, signal);
|
|
1039
|
+
signed.push(signedAction);
|
|
1040
|
+
const before = scopeOperationCount(working, scope);
|
|
1041
|
+
let next;
|
|
1042
|
+
try {
|
|
1043
|
+
next = module.reducer(working, signedAction, void 0, { protocolVersion: working.header.protocolVersions?.["base-reducer"] ?? predictionProtocolVersion });
|
|
1044
|
+
} catch (error) {
|
|
1045
|
+
throw new Error(`cannot sign action ${index} (${action.type}): the ${snapshot.header.documentType} reducer rejected it, so the rest of the batch cannot be predicted`, { cause: error });
|
|
1046
|
+
}
|
|
1047
|
+
const after = scopeOperationCount(next, scope);
|
|
1048
|
+
if (after !== before + 1) throw new Error(`cannot sign action ${index} (${action.type}): the reducer appended ${after - before} operations to ${scope}, expected exactly 1`);
|
|
1049
|
+
working = next;
|
|
1050
|
+
revision += 1;
|
|
1051
|
+
}
|
|
1052
|
+
return signed;
|
|
1053
|
+
}
|
|
1054
|
+
/**
|
|
1055
|
+
* How many operations a scope holds.
|
|
1056
|
+
*
|
|
1057
|
+
* The index signature types the array as always present, but a module's reducer
|
|
1058
|
+
* is arbitrary code: one that hands back a document without the scope should
|
|
1059
|
+
* produce the actionable error above, not a `TypeError` on `.length`.
|
|
1060
|
+
*/
|
|
1061
|
+
function scopeOperationCount(document, scope) {
|
|
1062
|
+
return document.operations[scope]?.length ?? 0;
|
|
1063
|
+
}
|
|
1064
|
+
/** The one scope a reactor job may span (`getSharedActionScope`). */
|
|
1065
|
+
function sharedScope(actions) {
|
|
1066
|
+
const scope = actions[0].scope;
|
|
1067
|
+
const mixed = actions.find((action) => action.scope !== scope);
|
|
1068
|
+
if (mixed) throw new Error(`cannot sign a batch spanning scopes "${scope}" and "${mixed.scope}": every action in one request must share a scope`);
|
|
1069
|
+
return scope;
|
|
1070
|
+
}
|
|
1071
|
+
function assertSupportedBatch(actions) {
|
|
1072
|
+
const unsupported = actions.find((action) => unsupportedBatchActions.has(action.type));
|
|
1073
|
+
if (unsupported) throw new Error(`cannot sign a batch containing ${unsupported.type}: it needs operation history or an alternate executor path, so send it as a single action`);
|
|
1074
|
+
}
|
|
1075
|
+
function throwIfAborted(signal, index) {
|
|
1076
|
+
if (signal?.aborted) throw new Error(`signing aborted before action ${index}`, { cause: signal.reason });
|
|
1077
|
+
}
|
|
1078
|
+
//#endregion
|
|
1079
|
+
//#region src/graphql-client/static-package-manager.ts
|
|
1080
|
+
/**
|
|
1081
|
+
* A fixed set of packages presented through the `IPackageManager` interface,
|
|
1082
|
+
* so the package-derived hooks - `useDocumentModelModules`,
|
|
1083
|
+
* `useDocumentModelModuleById`, `useEditorModules`, `useVetraPackages` - work
|
|
1084
|
+
* below `GraphQLReactorProvider` exactly as they do below Connect.
|
|
1085
|
+
*
|
|
1086
|
+
* Connect's package manager installs, updates and removes packages at runtime.
|
|
1087
|
+
* A light app instead declares its packages once, as code it imports itself
|
|
1088
|
+
* (every generated package root exports `manifest`, `documentModels`, `editors`
|
|
1089
|
+
* and `upgradeManifests` by name), so every mutating member throws and
|
|
1090
|
+
* `subscribe` never emits. For hand-picked modules without package artifacts,
|
|
1091
|
+
* {@link packageFromDocumentModels} wraps them in one synthetic package.
|
|
1092
|
+
*/
|
|
1093
|
+
var StaticPackageManager = class {
|
|
1094
|
+
registryUrl = null;
|
|
1095
|
+
packages;
|
|
1096
|
+
constructor(packages) {
|
|
1097
|
+
this.packages = [...packages];
|
|
1098
|
+
}
|
|
1099
|
+
/**
|
|
1100
|
+
* Resolves a module by document type across all packages. Mirrors the
|
|
1101
|
+
* registry's semantics (`IDocumentModelRegistry.getModule`): the LATEST
|
|
1102
|
+
* version wins, with `version ?? 1` as each module's default.
|
|
1103
|
+
*/
|
|
1104
|
+
load(documentType) {
|
|
1105
|
+
const modules = this.packages.flatMap((pkg) => pkg.documentModels);
|
|
1106
|
+
try {
|
|
1107
|
+
return Promise.resolve(resolveDocumentModelModule(modules, documentType));
|
|
1108
|
+
} catch (error) {
|
|
1109
|
+
return Promise.reject(error);
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
subscribe(_handler) {
|
|
1113
|
+
return () => {};
|
|
1114
|
+
}
|
|
1115
|
+
getPackageSource(_packageName) {
|
|
1116
|
+
return null;
|
|
1117
|
+
}
|
|
1118
|
+
getPackageVersion(_packageName) {}
|
|
1119
|
+
getRegistryPackages() {
|
|
1120
|
+
return [];
|
|
1121
|
+
}
|
|
1122
|
+
addPackage(_packageName) {
|
|
1123
|
+
throw staticPackageManagerError("addPackage");
|
|
1124
|
+
}
|
|
1125
|
+
addPackages(_packageNames) {
|
|
1126
|
+
throw staticPackageManagerError("addPackages");
|
|
1127
|
+
}
|
|
1128
|
+
removePackage(_name) {
|
|
1129
|
+
throw staticPackageManagerError("removePackage");
|
|
1130
|
+
}
|
|
1131
|
+
updateLocalPackage(_pkg, _version) {
|
|
1132
|
+
throw staticPackageManagerError("updateLocalPackage");
|
|
1133
|
+
}
|
|
1134
|
+
addLocalPackage(_name, _loadedPackage, _version) {
|
|
1135
|
+
throw staticPackageManagerError("addLocalPackage");
|
|
1136
|
+
}
|
|
1137
|
+
};
|
|
1138
|
+
/**
|
|
1139
|
+
* Resolves one document-model module out of a flat module list, with the same
|
|
1140
|
+
* rule the reactor registry uses (`IDocumentModelRegistry.getModule`, at
|
|
1141
|
+
* packages/reactor/src/registry/implementation.ts): with a `version`, only an
|
|
1142
|
+
* EXACT `version ?? 1` match resolves; without one, the LATEST version wins.
|
|
1143
|
+
*
|
|
1144
|
+
* Signing an existing document needs the exact rule. A document carries the
|
|
1145
|
+
* model version it was written with in `state.document.version`, and its
|
|
1146
|
+
* reducer is the only one whose operations that document's history can accept -
|
|
1147
|
+
* so "latest" is correct only when no version is asked for.
|
|
1148
|
+
*
|
|
1149
|
+
* Pure and dependency-free on purpose: this is reachable from the browser
|
|
1150
|
+
* entry, so it must not pull the reactor registry in as a value.
|
|
1151
|
+
*/
|
|
1152
|
+
function resolveDocumentModelModule(modules, documentType, version) {
|
|
1153
|
+
let latestModule;
|
|
1154
|
+
let latestVersion = -1;
|
|
1155
|
+
for (const module of modules) {
|
|
1156
|
+
if (module.documentModel.global.id !== documentType) continue;
|
|
1157
|
+
const moduleVersion = module.version ?? 1;
|
|
1158
|
+
if (version !== void 0 && moduleVersion === version) return module;
|
|
1159
|
+
if (moduleVersion > latestVersion) {
|
|
1160
|
+
latestVersion = moduleVersion;
|
|
1161
|
+
latestModule = module;
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
if (version === void 0 && latestModule) return latestModule;
|
|
1165
|
+
throw new Error(version === void 0 ? `Unknown document type: ${documentType}` : `Unknown document model version: ${documentType} v${version}` + (latestModule ? ` (available: ${availableVersions(modules, documentType).join(", ")})` : " (no module for this document type)"));
|
|
1166
|
+
}
|
|
1167
|
+
function availableVersions(modules, documentType) {
|
|
1168
|
+
return modules.filter((module) => module.documentModel.global.id === documentType).map((module) => module.version ?? 1).sort((a, b) => a - b);
|
|
1169
|
+
}
|
|
1170
|
+
function staticPackageManagerError(member) {
|
|
1171
|
+
return /* @__PURE__ */ new Error(`${member} is not supported: StaticPackageManager holds a fixed set of packages`);
|
|
1172
|
+
}
|
|
1173
|
+
/**
|
|
1174
|
+
* Wraps hand-picked modules in one synthetic `DocumentModelLib`, for apps that
|
|
1175
|
+
* assemble their model list from mixed sources instead of passing whole
|
|
1176
|
+
* packages. The manifest is fabricated - loose modules carry none - so prefer
|
|
1177
|
+
* the `packages` prop with the real package exports when you have them: a real
|
|
1178
|
+
* package also carries `editors`, which makes the editor hooks work.
|
|
1179
|
+
*/
|
|
1180
|
+
function packageFromDocumentModels(documentModels) {
|
|
1181
|
+
return {
|
|
1182
|
+
manifest: {
|
|
1183
|
+
name: "graphql-reactor-provider",
|
|
1184
|
+
description: "Document models passed to GraphQLReactorProvider"
|
|
1185
|
+
},
|
|
1186
|
+
documentModels,
|
|
1187
|
+
editors: []
|
|
1188
|
+
};
|
|
1189
|
+
}
|
|
1190
|
+
//#endregion
|
|
1191
|
+
//#region src/graphql-client/subgraph.ts
|
|
1192
|
+
/** What {@link describeGraphQLDocument} could not read off a document. */
|
|
1193
|
+
const unnamedOperation = "AnonymousOperation";
|
|
1194
|
+
const defaultOperationType = "query";
|
|
1195
|
+
/**
|
|
1196
|
+
* Derives a subgraph's endpoint from the Switchboard's GraphQL endpoint.
|
|
1197
|
+
*
|
|
1198
|
+
* Subgraphs are served next to the supergraph, at `<graphql base>/<name>`, so
|
|
1199
|
+
* `http://localhost:4001/graphql` plus `statements` is
|
|
1200
|
+
* `http://localhost:4001/graphql/statements`.
|
|
1201
|
+
*/
|
|
1202
|
+
function subgraphUrlFromGraphqlUrl(url, name) {
|
|
1203
|
+
const trimmed = name.trim();
|
|
1204
|
+
if (trimmed === "") throw new Error("a subgraph name is required");
|
|
1205
|
+
return `${url.replace(/\/+$/, "")}/${trimmed}`;
|
|
1206
|
+
}
|
|
1207
|
+
/**
|
|
1208
|
+
* Reads the operation name and type off a document, for the middleware.
|
|
1209
|
+
*
|
|
1210
|
+
* Nothing depends on getting this right - it is reported to the middleware, not
|
|
1211
|
+
* sent to the server - so an unreadable document falls back to an anonymous
|
|
1212
|
+
* query rather than throwing.
|
|
1213
|
+
*/
|
|
1214
|
+
function describeGraphQLDocument(document) {
|
|
1215
|
+
if (typeof document !== "string") {
|
|
1216
|
+
const operation = document.definitions.find((definition) => definition.kind === Kind.OPERATION_DEFINITION);
|
|
1217
|
+
if (!operation || operation.kind !== Kind.OPERATION_DEFINITION) return {
|
|
1218
|
+
operationName: unnamedOperation,
|
|
1219
|
+
operationType: defaultOperationType
|
|
1220
|
+
};
|
|
1221
|
+
return {
|
|
1222
|
+
operationName: operation.name?.value ?? unnamedOperation,
|
|
1223
|
+
operationType: operation.operation
|
|
1224
|
+
};
|
|
1225
|
+
}
|
|
1226
|
+
const match = /\b(query|mutation|subscription)\s+([_A-Za-z][_0-9A-Za-z]*)/.exec(document);
|
|
1227
|
+
if (!match) return {
|
|
1228
|
+
operationName: unnamedOperation,
|
|
1229
|
+
operationType: /\bmutation\b/.test(document) ? "mutation" : defaultOperationType
|
|
1230
|
+
};
|
|
1231
|
+
return {
|
|
1232
|
+
operationName: match[2],
|
|
1233
|
+
operationType: match[1]
|
|
1234
|
+
};
|
|
1235
|
+
}
|
|
1236
|
+
/**
|
|
1237
|
+
* Binds typed subgraph SDKs to one Switchboard's transport and auth.
|
|
1238
|
+
*
|
|
1239
|
+
* Every capability a Switchboard exposes lives behind the same base URL, so an
|
|
1240
|
+
* app should not have to build a second client - with a second copy of the auth
|
|
1241
|
+
* configuration - to reach a project's own subgraph. The registry derives the
|
|
1242
|
+
* endpoint, reuses one transport per subgraph and hands the code generator's
|
|
1243
|
+
* `getSdk` the very middleware the reactor operations already go through.
|
|
1244
|
+
*
|
|
1245
|
+
* Transport and typing only: subgraph results are not cached and produce no
|
|
1246
|
+
* change events.
|
|
1247
|
+
*/
|
|
1248
|
+
var SubgraphSdkRegistry = class {
|
|
1249
|
+
url;
|
|
1250
|
+
middleware;
|
|
1251
|
+
bindings = /* @__PURE__ */ new Map();
|
|
1252
|
+
constructor(url, middleware) {
|
|
1253
|
+
this.url = url;
|
|
1254
|
+
this.middleware = middleware;
|
|
1255
|
+
}
|
|
1256
|
+
/**
|
|
1257
|
+
* Returns the subgraph's SDK, building it on first use.
|
|
1258
|
+
*
|
|
1259
|
+
* The transport is kept per subgraph name, so repeated calls do not rebuild
|
|
1260
|
+
* it. The SDK itself is only reused when the same factory is passed again -
|
|
1261
|
+
* two different generated SDKs over one subgraph each get their own, rather
|
|
1262
|
+
* than the second call being handed the first one's methods.
|
|
1263
|
+
*/
|
|
1264
|
+
get(name, getSdk) {
|
|
1265
|
+
const binding = this.binding(name);
|
|
1266
|
+
if (binding.factory === getSdk) return binding.sdk;
|
|
1267
|
+
const sdk = getSdk(binding.transport, this.middleware);
|
|
1268
|
+
binding.factory = getSdk;
|
|
1269
|
+
binding.sdk = sdk;
|
|
1270
|
+
return sdk;
|
|
1271
|
+
}
|
|
1272
|
+
/** Returns the subgraph's endpoint, for diagnostics and tests. */
|
|
1273
|
+
urlFor(name) {
|
|
1274
|
+
return subgraphUrlFromGraphqlUrl(this.url, name);
|
|
1275
|
+
}
|
|
1276
|
+
binding(name) {
|
|
1277
|
+
const existing = this.bindings.get(name);
|
|
1278
|
+
if (existing) return existing;
|
|
1279
|
+
const binding = { transport: new GraphQLClient(this.urlFor(name)) };
|
|
1280
|
+
this.bindings.set(name, binding);
|
|
1281
|
+
return binding;
|
|
1282
|
+
}
|
|
1283
|
+
};
|
|
1284
|
+
//#endregion
|
|
1285
|
+
//#region src/graphql-client/subscriptions.ts
|
|
1286
|
+
/**
|
|
1287
|
+
* Opens one `documentChanges` subscription and feeds every event to `onEvent`.
|
|
1288
|
+
*
|
|
1289
|
+
* The subscription is a firehose: no `search` argument, so the server sends
|
|
1290
|
+
* every change the connection is allowed to see and the caller filters. This is
|
|
1291
|
+
* the only module that knows about `graphql-ws`.
|
|
1292
|
+
*
|
|
1293
|
+
* Returns the stop function, which cancels the subscription and closes the
|
|
1294
|
+
* socket. It is safe to call more than once.
|
|
1295
|
+
*/
|
|
1296
|
+
function startDocumentChangesSubscription(options) {
|
|
1297
|
+
const client = createClient({
|
|
1298
|
+
url: options.wsUrl,
|
|
1299
|
+
connectionParams: options.connectionParams
|
|
1300
|
+
});
|
|
1301
|
+
const unsubscribe = client.subscribe({
|
|
1302
|
+
operationName: "DocumentChanges",
|
|
1303
|
+
query: print(DocumentChangesDocument)
|
|
1304
|
+
}, {
|
|
1305
|
+
next: (result) => {
|
|
1306
|
+
const event = result.data?.documentChanges;
|
|
1307
|
+
if (event) options.onEvent(event);
|
|
1308
|
+
},
|
|
1309
|
+
error: (error) => options.onError(error),
|
|
1310
|
+
complete: () => void 0
|
|
1311
|
+
});
|
|
1312
|
+
let stopped = false;
|
|
1313
|
+
return () => {
|
|
1314
|
+
if (stopped) return;
|
|
1315
|
+
stopped = true;
|
|
1316
|
+
unsubscribe();
|
|
1317
|
+
client.dispose();
|
|
1318
|
+
};
|
|
1319
|
+
}
|
|
1320
|
+
/**
|
|
1321
|
+
* Derives the websocket endpoint from the GraphQL http endpoint.
|
|
1322
|
+
*
|
|
1323
|
+
* `http://host/graphql` becomes `ws://host/graphql/subscriptions`, which is
|
|
1324
|
+
* where the Switchboard mounts its websocket server (`packages/reactor-api`
|
|
1325
|
+
* `startServer`). A URL that already speaks `ws`/`wss` only gets the path.
|
|
1326
|
+
*/
|
|
1327
|
+
function subscriptionsUrlFromGraphqlUrl(url) {
|
|
1328
|
+
const trimmed = url.replace(/\/+$/, "");
|
|
1329
|
+
return `${trimmed.startsWith("https://") ? `wss://${trimmed.slice(8)}` : trimmed.startsWith("http://") ? `ws://${trimmed.slice(7)}` : trimmed}/subscriptions`;
|
|
1330
|
+
}
|
|
1331
|
+
/**
|
|
1332
|
+
* Turns a bearer token provider into `connectionParams`.
|
|
1333
|
+
*
|
|
1334
|
+
* The header key is the lowercase `authorization` the Switchboard's websocket
|
|
1335
|
+
* context factory reads. Without a token the payload is empty: an open
|
|
1336
|
+
* Switchboard serves anonymous subscribers.
|
|
1337
|
+
*/
|
|
1338
|
+
function makeAuthConnectionParams(tokenProvider) {
|
|
1339
|
+
return async () => {
|
|
1340
|
+
const token = await tokenProvider();
|
|
1341
|
+
if (!token) return {};
|
|
1342
|
+
return { authorization: `Bearer ${token}` };
|
|
1343
|
+
};
|
|
1344
|
+
}
|
|
1345
|
+
//#endregion
|
|
1346
|
+
//#region src/graphql-client/graphql-reactor-client.ts
|
|
1347
|
+
/** Paging defaults, matching the reactor's own client. */
|
|
1348
|
+
const defaultPaging = {
|
|
1349
|
+
cursor: "0",
|
|
1350
|
+
limit: 100
|
|
1351
|
+
};
|
|
1352
|
+
/**
|
|
1353
|
+
* The marker {@link isGraphQLReactorClient} looks for.
|
|
1354
|
+
*
|
|
1355
|
+
* A page can end up with two copies of this module - an app that bundles a
|
|
1356
|
+
* package carrying its own copy, or a dev server that hot-replaced it - and
|
|
1357
|
+
* then `instanceof` compares an instance against the other copy's class object
|
|
1358
|
+
* and answers `false`. A branded property survives both.
|
|
1359
|
+
*/
|
|
1360
|
+
const graphQLReactorClientBrand = "__powerhouseGraphQLReactorClient";
|
|
1361
|
+
/**
|
|
1362
|
+
* A light implementation of {@link IReactorBrowserClient} that talks plain
|
|
1363
|
+
* GraphQL to a Switchboard, with no reactor in the bundle.
|
|
1364
|
+
*
|
|
1365
|
+
* It fills the same `window.ph` slots the full in-browser reactor client fills,
|
|
1366
|
+
* so the reactor-browser hooks cannot tell the two apart.
|
|
1367
|
+
*/
|
|
1368
|
+
var GraphQLReactorClient = class {
|
|
1369
|
+
/** See {@link isGraphQLReactorClient}. */
|
|
1370
|
+
[graphQLReactorClientBrand] = true;
|
|
1371
|
+
sdk;
|
|
1372
|
+
subgraphs;
|
|
1373
|
+
listeners = [];
|
|
1374
|
+
tokenProvider;
|
|
1375
|
+
subscriptionsUrl;
|
|
1376
|
+
documentModels;
|
|
1377
|
+
signer;
|
|
1378
|
+
stopRealtime;
|
|
1379
|
+
realtimeStarted = false;
|
|
1380
|
+
realtimeGeneration = 0;
|
|
1381
|
+
realtimeErrorLogged = false;
|
|
1382
|
+
constructor(options) {
|
|
1383
|
+
this.tokenProvider = options.tokenProvider ?? ambientRenownTokenProvider;
|
|
1384
|
+
this.documentModels = [...options.documentModels ?? []];
|
|
1385
|
+
this.signer = options.signer;
|
|
1386
|
+
this.subscriptionsUrl = options.realtime === false ? void 0 : options.subscriptionsUrl ?? subscriptionsUrlFromGraphqlUrl(options.url);
|
|
1387
|
+
const middleware = makeAuthMiddleware(this.tokenProvider);
|
|
1388
|
+
this.sdk = options.graphqlClient ?? createClient$1(options.url, middleware);
|
|
1389
|
+
this.subgraphs = new SubgraphSdkRegistry(options.url, middleware);
|
|
1390
|
+
}
|
|
1391
|
+
async get(identifier, view, signal) {
|
|
1392
|
+
const viewInput = viewFilterInputFromViewFilter(view);
|
|
1393
|
+
const document = (await this.sdk.GetDocument({
|
|
1394
|
+
identifier,
|
|
1395
|
+
view: viewInput
|
|
1396
|
+
}, void 0, signal)).document;
|
|
1397
|
+
if (!document) throw new Error(`Document not found: ${identifier}`);
|
|
1398
|
+
return phDocumentFromGetDocument(document.document, view?.branch);
|
|
1399
|
+
}
|
|
1400
|
+
async getOperations(documentIdentifier, view, filter, paging, signal) {
|
|
1401
|
+
const viewInput = viewFilterInputFromViewFilter(view);
|
|
1402
|
+
const operationsFilter = {
|
|
1403
|
+
documentId: documentIdentifier,
|
|
1404
|
+
branch: viewInput?.branch,
|
|
1405
|
+
scopes: viewInput?.scopes,
|
|
1406
|
+
actionTypes: filter?.actionTypes,
|
|
1407
|
+
timestampFrom: filter?.timestampFrom,
|
|
1408
|
+
timestampTo: filter?.timestampTo,
|
|
1409
|
+
sinceRevision: filter?.sinceRevision
|
|
1410
|
+
};
|
|
1411
|
+
const page = (await this.sdk.GetDocumentOperations({
|
|
1412
|
+
filter: operationsFilter,
|
|
1413
|
+
paging: pagingInputFromPaging(paging)
|
|
1414
|
+
}, void 0, signal)).documentOperations;
|
|
1415
|
+
const nextCursor = page.hasNextPage ? page.cursor ?? void 0 : void 0;
|
|
1416
|
+
const effectivePaging = paging ?? defaultPaging;
|
|
1417
|
+
return {
|
|
1418
|
+
results: page.items.map((item) => remoteOperationToLocal(item)),
|
|
1419
|
+
options: effectivePaging,
|
|
1420
|
+
nextCursor,
|
|
1421
|
+
totalCount: page.totalCount,
|
|
1422
|
+
next: nextCursor ? () => this.getOperations(documentIdentifier, view, filter, {
|
|
1423
|
+
cursor: nextCursor,
|
|
1424
|
+
limit: effectivePaging.limit
|
|
1425
|
+
}, signal) : void 0
|
|
1426
|
+
};
|
|
1427
|
+
}
|
|
1428
|
+
async create(document, parentIdentifier, signal) {
|
|
1429
|
+
const created = phDocumentFromGetDocument((await this.sdk.CreateDocument({
|
|
1430
|
+
document,
|
|
1431
|
+
parentIdentifier
|
|
1432
|
+
}, void 0, signal)).createDocument);
|
|
1433
|
+
this.emitChange({
|
|
1434
|
+
type: DOCUMENT_CHANGE_TYPE.Created,
|
|
1435
|
+
documents: [created]
|
|
1436
|
+
});
|
|
1437
|
+
return created;
|
|
1438
|
+
}
|
|
1439
|
+
/**
|
|
1440
|
+
* Pushes actions to the Switchboard and returns the updated document.
|
|
1441
|
+
*
|
|
1442
|
+
* The document is fetched first: the response is the baseline for the
|
|
1443
|
+
* `sinceRevision` filter that narrows the returned operations to the ones
|
|
1444
|
+
* this call appended, and it carries the state hash a signed action is
|
|
1445
|
+
* stamped with.
|
|
1446
|
+
*
|
|
1447
|
+
* The emitted `Updated` event carries the document's id, never the
|
|
1448
|
+
* identifier this was called with: subscribers - including `DocumentCache` -
|
|
1449
|
+
* work in id space.
|
|
1450
|
+
*/
|
|
1451
|
+
async execute(documentIdentifier, branch, actions, signal) {
|
|
1452
|
+
const document = await this.get(documentIdentifier, { branch }, signal);
|
|
1453
|
+
const variables = {
|
|
1454
|
+
documentIdentifier,
|
|
1455
|
+
actions: (await prepareActionsForPush(actions, document, this.documentModels, this.signer, signal)).map(toTransportAction),
|
|
1456
|
+
sinceRevision: sinceRevisionForActions(document, actions),
|
|
1457
|
+
scopes: scopesForActions(actions),
|
|
1458
|
+
branch
|
|
1459
|
+
};
|
|
1460
|
+
const mutated = (await this.sdk.RunDocument({
|
|
1461
|
+
operationName: "MutateDocumentWithOperations",
|
|
1462
|
+
operationType: "mutation",
|
|
1463
|
+
document: MutateDocumentWithOperationsDocument,
|
|
1464
|
+
variables,
|
|
1465
|
+
signal
|
|
1466
|
+
})).mutateDocument;
|
|
1467
|
+
const updated = phDocumentFromMutation(mutated, mutated.operations?.items ?? [], branch);
|
|
1468
|
+
this.emitChange({
|
|
1469
|
+
type: DOCUMENT_CHANGE_TYPE.Updated,
|
|
1470
|
+
documents: [updated]
|
|
1471
|
+
});
|
|
1472
|
+
return updated;
|
|
1473
|
+
}
|
|
1474
|
+
/**
|
|
1475
|
+
* Deletes a document and announces it as a `Deleted` event.
|
|
1476
|
+
*
|
|
1477
|
+
* Pass a document id. The server resolves a slug just as happily, but
|
|
1478
|
+
* `deleteDocument` returns a boolean - there is no document left to read an
|
|
1479
|
+
* id off - so the identifier is announced verbatim on `context.childId`. A
|
|
1480
|
+
* subscriber keyed by id, `DocumentCache` included, therefore only reacts to
|
|
1481
|
+
* a delete by id.
|
|
1482
|
+
*/
|
|
1483
|
+
async deleteDocument(identifier, propagate, signal) {
|
|
1484
|
+
await this.sdk.DeleteDocument({
|
|
1485
|
+
identifier,
|
|
1486
|
+
propagate: propagationModeInput(propagate)
|
|
1487
|
+
}, void 0, signal);
|
|
1488
|
+
this.emitChange({
|
|
1489
|
+
type: DOCUMENT_CHANGE_TYPE.Deleted,
|
|
1490
|
+
documents: [],
|
|
1491
|
+
context: { childId: identifier }
|
|
1492
|
+
});
|
|
1493
|
+
}
|
|
1494
|
+
/**
|
|
1495
|
+
* Registers a change subscriber and returns its unsubscribe function.
|
|
1496
|
+
*
|
|
1497
|
+
* Two things produce the events delivered here. Every successful `create`,
|
|
1498
|
+
* `execute` and `deleteDocument` emits one, which is what makes a
|
|
1499
|
+
* `DocumentCache` built on this client invalidate after a dispatch. And,
|
|
1500
|
+
* unless realtime is switched off, the first call to this method opens a
|
|
1501
|
+
* websocket to the Switchboard and every change it reports - from another
|
|
1502
|
+
* user, another tab or a server-side processor - is emitted too. A write of
|
|
1503
|
+
* this client's own is therefore announced twice, once locally and once by
|
|
1504
|
+
* the server; the cache refetches twice, which is harmless.
|
|
1505
|
+
*
|
|
1506
|
+
* A websocket that cannot be opened or is refused is logged once and then
|
|
1507
|
+
* ignored: realtime is an enhancement, never a dependency. The failed socket
|
|
1508
|
+
* is closed, so the next subscriber to arrive tries again.
|
|
1509
|
+
*
|
|
1510
|
+
* `view` is accepted for interface compatibility and ignored: the emitted
|
|
1511
|
+
* documents are whatever the write returned, so there is nothing to re-scope.
|
|
1512
|
+
*
|
|
1513
|
+
* Events live in id space: `search.ids` is matched against the resolved
|
|
1514
|
+
* document id, never against the identifier the write was issued with. The
|
|
1515
|
+
* one exception is `deleteDocument`, which has no document to resolve an id
|
|
1516
|
+
* from - see its own note. A `DocumentCache` on this client must therefore
|
|
1517
|
+
* be read by id: a slug-keyed entry is never invalidated.
|
|
1518
|
+
*/
|
|
1519
|
+
subscribe(search, callback, view) {
|
|
1520
|
+
const listener = {
|
|
1521
|
+
search,
|
|
1522
|
+
callback
|
|
1523
|
+
};
|
|
1524
|
+
this.listeners.push(listener);
|
|
1525
|
+
this.startRealtime();
|
|
1526
|
+
return () => {
|
|
1527
|
+
const index = this.listeners.indexOf(listener);
|
|
1528
|
+
if (index !== -1) this.listeners.splice(index, 1);
|
|
1529
|
+
};
|
|
1530
|
+
}
|
|
1531
|
+
/**
|
|
1532
|
+
* Closes the realtime socket, if one was opened.
|
|
1533
|
+
*
|
|
1534
|
+
* Registered subscribers are left in place: they still receive the events
|
|
1535
|
+
* this client produces itself.
|
|
1536
|
+
*
|
|
1537
|
+
* Disposing is NOT final - a later `subscribe` opens a new socket. React
|
|
1538
|
+
* remounts a tree by running an effect's cleanup and then the effect again,
|
|
1539
|
+
* on the same client, and `StrictMode` does exactly that on every mount; a
|
|
1540
|
+
* terminal flag here would leave those pages without realtime for good, with
|
|
1541
|
+
* nothing logged.
|
|
1542
|
+
*/
|
|
1543
|
+
dispose() {
|
|
1544
|
+
this.teardownRealtime();
|
|
1545
|
+
}
|
|
1546
|
+
/**
|
|
1547
|
+
* Binds a project's generated SDK to this client's transport and auth.
|
|
1548
|
+
*
|
|
1549
|
+
* Subgraphs are served next to the reactor's own supergraph, so the endpoint
|
|
1550
|
+
* is derived from the client's `url` by appending the name, and the SDK is
|
|
1551
|
+
* handed the same auth middleware every reactor call goes through. An app
|
|
1552
|
+
* therefore configures one URL and one token provider, and reaches every
|
|
1553
|
+
* capability of its Switchboard through this one client.
|
|
1554
|
+
*
|
|
1555
|
+
* `getSdk` is the function a GraphQL code generator emits for the subgraph's
|
|
1556
|
+
* schema - see the README in this folder for the codegen recipe.
|
|
1557
|
+
*
|
|
1558
|
+
* Transport and typing only. Subgraph results are not cached, produce no
|
|
1559
|
+
* change events and never reach `useDocument`.
|
|
1560
|
+
*/
|
|
1561
|
+
subgraph(name, getSdk) {
|
|
1562
|
+
return this.subgraphs.get(name, getSdk);
|
|
1563
|
+
}
|
|
1564
|
+
/**
|
|
1565
|
+
* Runs a one-off GraphQL document against the Switchboard's own endpoint,
|
|
1566
|
+
* through the same transport and auth as everything else.
|
|
1567
|
+
*
|
|
1568
|
+
* The escape hatch for an operation not worth generating an SDK for. Pass a
|
|
1569
|
+
* document a code generator typed and the result type follows; pass a plain
|
|
1570
|
+
* `gql` document or a string and name the result type at the call site.
|
|
1571
|
+
*/
|
|
1572
|
+
async request(document, variables, options) {
|
|
1573
|
+
const described = describeGraphQLDocument(document);
|
|
1574
|
+
return this.sdk.RunDocument({
|
|
1575
|
+
operationName: options?.operationName ?? described.operationName,
|
|
1576
|
+
operationType: options?.operationType ?? described.operationType,
|
|
1577
|
+
document,
|
|
1578
|
+
variables,
|
|
1579
|
+
signal: options?.signal
|
|
1580
|
+
});
|
|
1581
|
+
}
|
|
1582
|
+
/**
|
|
1583
|
+
* Opens the realtime socket on the first subscriber.
|
|
1584
|
+
*
|
|
1585
|
+
* The subscription is a firehose - no `search` argument - and this client
|
|
1586
|
+
* applies each subscriber's own filter on the way out, exactly as it does for
|
|
1587
|
+
* its own emissions.
|
|
1588
|
+
*/
|
|
1589
|
+
startRealtime() {
|
|
1590
|
+
if (!this.subscriptionsUrl || this.realtimeStarted) return;
|
|
1591
|
+
this.realtimeStarted = true;
|
|
1592
|
+
const generation = this.realtimeGeneration;
|
|
1593
|
+
const stop = startDocumentChangesSubscription({
|
|
1594
|
+
wsUrl: this.subscriptionsUrl,
|
|
1595
|
+
connectionParams: makeAuthConnectionParams(this.tokenProvider),
|
|
1596
|
+
onEvent: (event) => this.emitServerEvent(event),
|
|
1597
|
+
onError: (error) => this.handleRealtimeFailure(generation, error)
|
|
1598
|
+
});
|
|
1599
|
+
if (this.realtimeGeneration !== generation) {
|
|
1600
|
+
stop();
|
|
1601
|
+
return;
|
|
1602
|
+
}
|
|
1603
|
+
this.stopRealtime = stop;
|
|
1604
|
+
}
|
|
1605
|
+
/**
|
|
1606
|
+
* Gives up on a failed socket so that a later subscriber can try again.
|
|
1607
|
+
*
|
|
1608
|
+
* `graphql-ws` retries on its own and only reports here once it has given up,
|
|
1609
|
+
* or once the server refused the subscription outright - which is what an
|
|
1610
|
+
* auth-enabled Switchboard does to an anonymous subscriber. Keeping the dead
|
|
1611
|
+
* stop function would make every later `subscribe` a no-op, so realtime would
|
|
1612
|
+
* stay off for the life of the page even after the user signs in.
|
|
1613
|
+
*
|
|
1614
|
+
* The generation stamp discards a report from a socket that has already been
|
|
1615
|
+
* replaced or disposed.
|
|
1616
|
+
*/
|
|
1617
|
+
handleRealtimeFailure(generation, error) {
|
|
1618
|
+
if (generation !== this.realtimeGeneration) return;
|
|
1619
|
+
this.teardownRealtime();
|
|
1620
|
+
this.logRealtimeError(error);
|
|
1621
|
+
}
|
|
1622
|
+
/** Closes the socket and lets a later subscriber open a new one. */
|
|
1623
|
+
teardownRealtime() {
|
|
1624
|
+
this.realtimeGeneration += 1;
|
|
1625
|
+
this.realtimeStarted = false;
|
|
1626
|
+
this.stopRealtime?.();
|
|
1627
|
+
this.stopRealtime = void 0;
|
|
1628
|
+
}
|
|
1629
|
+
/** Maps a server event onto the client-side event and emits it. */
|
|
1630
|
+
emitServerEvent(event) {
|
|
1631
|
+
this.emitChange({
|
|
1632
|
+
type: documentChangeTypes[event.type],
|
|
1633
|
+
documents: event.documents.map((document) => phDocumentFromGetDocument(document)),
|
|
1634
|
+
context: event.context ? {
|
|
1635
|
+
parentId: event.context.parentId ?? void 0,
|
|
1636
|
+
childId: event.context.childId ?? void 0
|
|
1637
|
+
} : void 0
|
|
1638
|
+
});
|
|
1639
|
+
}
|
|
1640
|
+
/**
|
|
1641
|
+
* Reports a broken realtime socket once per client, however many sockets it
|
|
1642
|
+
* goes through.
|
|
1643
|
+
*
|
|
1644
|
+
* `graphql-ws` retries on its own, and a Switchboard without a websocket
|
|
1645
|
+
* endpoint would otherwise fill the console for as long as the page is open.
|
|
1646
|
+
*/
|
|
1647
|
+
logRealtimeError(error) {
|
|
1648
|
+
if (this.realtimeErrorLogged) return;
|
|
1649
|
+
this.realtimeErrorLogged = true;
|
|
1650
|
+
logger.warn("GraphQLReactorClient: realtime document changes are unavailable, falling back to local change events", error);
|
|
1651
|
+
}
|
|
1652
|
+
/**
|
|
1653
|
+
* Delivers an event to every matching subscriber.
|
|
1654
|
+
*
|
|
1655
|
+
* A subscriber that throws is logged and skipped so one bad listener cannot
|
|
1656
|
+
* stop the others from being notified.
|
|
1657
|
+
*/
|
|
1658
|
+
emitChange(event) {
|
|
1659
|
+
for (const listener of [...this.listeners]) {
|
|
1660
|
+
if (!eventMatchesSearch(event, listener.search)) continue;
|
|
1661
|
+
try {
|
|
1662
|
+
listener.callback(event);
|
|
1663
|
+
} catch (error) {
|
|
1664
|
+
logger.error("GraphQLReactorClient: a document change subscriber threw", error);
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
};
|
|
1669
|
+
/**
|
|
1670
|
+
* Whether a value is a {@link GraphQLReactorClient}, including one built by
|
|
1671
|
+
* another copy of this module.
|
|
1672
|
+
*
|
|
1673
|
+
* Use this rather than `instanceof`: the brand it tests for is shared by every
|
|
1674
|
+
* copy of the class, so a client built by a hot-replaced module or by a package
|
|
1675
|
+
* that bundled its own copy is still recognised.
|
|
1676
|
+
*/
|
|
1677
|
+
function isGraphQLReactorClient(client) {
|
|
1678
|
+
return client?.[graphQLReactorClientBrand] === true;
|
|
1679
|
+
}
|
|
1680
|
+
/** The schema's change types, mapped onto the reactor's. */
|
|
1681
|
+
const documentChangeTypes = {
|
|
1682
|
+
[DocumentChangeType.Created]: DOCUMENT_CHANGE_TYPE.Created,
|
|
1683
|
+
[DocumentChangeType.Deleted]: DOCUMENT_CHANGE_TYPE.Deleted,
|
|
1684
|
+
[DocumentChangeType.Updated]: DOCUMENT_CHANGE_TYPE.Updated,
|
|
1685
|
+
[DocumentChangeType.ParentAdded]: DOCUMENT_CHANGE_TYPE.ParentAdded,
|
|
1686
|
+
[DocumentChangeType.ParentRemoved]: DOCUMENT_CHANGE_TYPE.ParentRemoved,
|
|
1687
|
+
[DocumentChangeType.ChildAdded]: DOCUMENT_CHANGE_TYPE.ChildAdded,
|
|
1688
|
+
[DocumentChangeType.ChildRemoved]: DOCUMENT_CHANGE_TYPE.ChildRemoved
|
|
1689
|
+
};
|
|
1690
|
+
/**
|
|
1691
|
+
* Decides whether an event reaches a subscriber.
|
|
1692
|
+
*
|
|
1693
|
+
* Every populated field of the search filter is an AND condition, matching the
|
|
1694
|
+
* reactor's own subscription manager - which also treats an empty array as a
|
|
1695
|
+
* filter that nothing satisfies. `parentId` is a drive concept and is ignored.
|
|
1696
|
+
*
|
|
1697
|
+
* A `Deleted` event carries no documents, so it is matched on its
|
|
1698
|
+
* `context.childId` for identifier filters and delivered unconditionally to
|
|
1699
|
+
* type and slug filters: there is no document left to check them against.
|
|
1700
|
+
*/
|
|
1701
|
+
function eventMatchesSearch(event, search) {
|
|
1702
|
+
const deleted = event.type === DOCUMENT_CHANGE_TYPE.Deleted;
|
|
1703
|
+
const { ids, slugs, type } = search;
|
|
1704
|
+
if (ids) {
|
|
1705
|
+
if (!(deleted ? [event.context?.childId] : event.documents.map((document) => document.header.id)).some((id) => id !== void 0 && ids.includes(id))) return false;
|
|
1706
|
+
}
|
|
1707
|
+
if (slugs && !deleted) {
|
|
1708
|
+
if (!event.documents.map((document) => document.header.slug).some((slug) => slugs.includes(slug))) return false;
|
|
1709
|
+
}
|
|
1710
|
+
if (type && !deleted) {
|
|
1711
|
+
if (!event.documents.map((document) => document.header.documentType).includes(type)) return false;
|
|
1712
|
+
}
|
|
1713
|
+
return true;
|
|
1714
|
+
}
|
|
1715
|
+
/**
|
|
1716
|
+
* Maps a reactor `ViewFilter` onto the GraphQL `ViewFilterInput`.
|
|
1717
|
+
*
|
|
1718
|
+
* Point-in-time reads are rejected: the Switchboard read API has no revision
|
|
1719
|
+
* argument, so silently returning the head state would be wrong.
|
|
1720
|
+
*/
|
|
1721
|
+
function viewFilterInputFromViewFilter(view) {
|
|
1722
|
+
if (!view) return;
|
|
1723
|
+
if (view.revision !== void 0) throw new Error("point-in-time views are not supported by GraphQLReactorClient");
|
|
1724
|
+
if (view.branch === void 0 && view.scopes === void 0) return;
|
|
1725
|
+
return {
|
|
1726
|
+
branch: view.branch,
|
|
1727
|
+
scopes: view.scopes
|
|
1728
|
+
};
|
|
1729
|
+
}
|
|
1730
|
+
function pagingInputFromPaging(paging) {
|
|
1731
|
+
if (!paging) return;
|
|
1732
|
+
return {
|
|
1733
|
+
cursor: paging.cursor,
|
|
1734
|
+
limit: paging.limit
|
|
1735
|
+
};
|
|
1736
|
+
}
|
|
1737
|
+
/**
|
|
1738
|
+
* The reactor and the GraphQL schema disagree on the enum: the reactor's
|
|
1739
|
+
* `None` is the schema's `ORPHAN`. This is the inverse of the server's
|
|
1740
|
+
* `toReactorPropagationMode`.
|
|
1741
|
+
*/
|
|
1742
|
+
const propagationModeInputs = {
|
|
1743
|
+
cascade: PropagationMode.Cascade,
|
|
1744
|
+
none: PropagationMode.Orphan
|
|
1745
|
+
};
|
|
1746
|
+
function propagationModeInput(propagate) {
|
|
1747
|
+
return propagate === void 0 ? void 0 : propagationModeInputs[propagate];
|
|
1748
|
+
}
|
|
1749
|
+
/**
|
|
1750
|
+
* Stamps and signs the actions about to be pushed.
|
|
1751
|
+
*
|
|
1752
|
+
* With no signer the actions go out exactly as given, batch or not - this
|
|
1753
|
+
* client does not require signatures. With one, every action is signed:
|
|
1754
|
+
* {@link prepareSignedActions} predicts the chain a batch will produce by
|
|
1755
|
+
* running the document's own reducer between signatures, which is why a batch
|
|
1756
|
+
* needs the module matching the document's type and exact version.
|
|
1757
|
+
*
|
|
1758
|
+
* A batch that cannot be predicted - no matching module, mixed scopes, an
|
|
1759
|
+
* action needing history - throws here, before the mutation. Sending it
|
|
1760
|
+
* unsigned instead would silently drop the signatures the caller asked for.
|
|
1761
|
+
*/
|
|
1762
|
+
async function prepareActionsForPush(actions, document, documentModels, explicitSigner, signal) {
|
|
1763
|
+
const signer = explicitSigner ?? resolveAmbientSigner();
|
|
1764
|
+
if (!signer || actions.length === 0) return actions;
|
|
1765
|
+
return prepareSignedActions(actions, document, signer, actions.length > 1 ? resolveDocumentModelModule(documentModels, document.header.documentType, documentModelVersion(document)) : void 0, signal);
|
|
1766
|
+
}
|
|
1767
|
+
/**
|
|
1768
|
+
* The document-model version the document's actions must be reduced with.
|
|
1769
|
+
*
|
|
1770
|
+
* `state` arrives as JSON over GraphQL, so its `document` scope is only as
|
|
1771
|
+
* reliable as the server that sent it - one written before that scope existed
|
|
1772
|
+
* carries no version at all. `normalizeDocumentModelVersion` maps that, and 0,
|
|
1773
|
+
* to 1: the same rule `SimpleJobExecutor` applies before asking the registry
|
|
1774
|
+
* for a module, so client and server cannot resolve different reducers.
|
|
1775
|
+
*/
|
|
1776
|
+
function documentModelVersion(document) {
|
|
1777
|
+
const documentScope = document.state.document;
|
|
1778
|
+
return normalizeDocumentModelVersion(documentScope?.version);
|
|
1779
|
+
}
|
|
1780
|
+
/** Resolves the signer of the logged-in user, if there is one. */
|
|
1781
|
+
function resolveAmbientSigner() {
|
|
1782
|
+
if (typeof window === "undefined") return;
|
|
1783
|
+
const renown = window.ph?.renown;
|
|
1784
|
+
return renown?.user ? renown.signer : void 0;
|
|
1785
|
+
}
|
|
1786
|
+
/**
|
|
1787
|
+
* The revision the pushed operations start from: the lowest head revision
|
|
1788
|
+
* across the scopes the actions target.
|
|
1789
|
+
*
|
|
1790
|
+
* The baseline is only meaningful together with {@link scopesForActions}: a
|
|
1791
|
+
* batch that touches a scope at revision 1 and one at revision 5000 would
|
|
1792
|
+
* otherwise re-download the whole history of every scope of the document.
|
|
1793
|
+
*/
|
|
1794
|
+
function sinceRevisionForActions(document, actions) {
|
|
1795
|
+
const revisions = actions.map((action) => document.header.revision[action.scope] ?? 0);
|
|
1796
|
+
return revisions.length > 0 ? Math.min(...revisions) : 0;
|
|
1797
|
+
}
|
|
1798
|
+
/** The distinct scopes the pushed actions target, in first-seen order. */
|
|
1799
|
+
function scopesForActions(actions) {
|
|
1800
|
+
if (actions.length === 0) return;
|
|
1801
|
+
return [...new Set(actions.map((action) => action.scope))];
|
|
1802
|
+
}
|
|
1803
|
+
//#endregion
|
|
1804
|
+
//#region src/hooks/config/editor.ts
|
|
1805
|
+
const isExternalControlsEnabledEventFunctions = makePHEventFunctions("isExternalControlsEnabled");
|
|
1806
|
+
/** Sets whether external controls are enabled for a given editor. */
|
|
1807
|
+
const setIsExternalControlsEnabled = isExternalControlsEnabledEventFunctions.setValue;
|
|
1808
|
+
/** Gets whether external controls are enabled for a given editor. */
|
|
1809
|
+
const useIsExternalControlsEnabled = isExternalControlsEnabledEventFunctions.useValue;
|
|
1810
|
+
/** Adds an event handler for when the external controls enabled state changes. */
|
|
1811
|
+
const addIsExternalControlsEnabledEventHandler = isExternalControlsEnabledEventFunctions.addEventHandler;
|
|
1812
|
+
const isDragAndDropEnabledEventFunctions = makePHEventFunctions("isDragAndDropEnabled");
|
|
1813
|
+
/** Sets whether drag and drop is enabled for a given app. */
|
|
1814
|
+
const setIsDragAndDropEnabled = isDragAndDropEnabledEventFunctions.setValue;
|
|
1815
|
+
/** Gets whether drag and drop is enabled for a given app. */
|
|
1816
|
+
const useIsDragAndDropEnabled = isDragAndDropEnabledEventFunctions.useValue;
|
|
1817
|
+
/** Adds an event handler for when the drag and drop enabled state changes. */
|
|
1818
|
+
const addIsDragAndDropEnabledEventHandler = isDragAndDropEnabledEventFunctions.addEventHandler;
|
|
1819
|
+
const allowedDocumentTypesEventFunctions = makePHEventFunctions("allowedDocumentTypes");
|
|
1820
|
+
/** Sets the allowed document types for a given app. */
|
|
1821
|
+
const setAllowedDocumentTypes = allowedDocumentTypesEventFunctions.setValue;
|
|
1822
|
+
/** Defines the document types a drive supports.
|
|
1823
|
+
*
|
|
1824
|
+
* Defaults to all of the document types registered in the reactor.
|
|
1825
|
+
*/
|
|
1826
|
+
function useAllowedDocumentTypes() {
|
|
1827
|
+
return allowedDocumentTypesEventFunctions.useValue();
|
|
1828
|
+
}
|
|
1829
|
+
/** Adds an event handler for when the allowed document types for a given app changes. */
|
|
1830
|
+
const addAllowedDocumentTypesEventHandler = allowedDocumentTypesEventFunctions.addEventHandler;
|
|
1831
|
+
const phAppConfigSetters = {
|
|
1832
|
+
allowedDocumentTypes: setAllowedDocumentTypes,
|
|
1833
|
+
isDragAndDropEnabled: setIsDragAndDropEnabled
|
|
1834
|
+
};
|
|
1835
|
+
const phDocumentEditorConfigSetters = { isExternalControlsEnabled: setIsExternalControlsEnabled };
|
|
1836
|
+
const phAppConfigHooks = {
|
|
1837
|
+
allowedDocumentTypes: useAllowedDocumentTypes,
|
|
1838
|
+
isDragAndDropEnabled: useIsDragAndDropEnabled
|
|
1839
|
+
};
|
|
1840
|
+
const phDocumentEditorConfigHooks = { isExternalControlsEnabled: useIsExternalControlsEnabled };
|
|
1841
|
+
//#endregion
|
|
1842
|
+
//#region src/hooks/config/connect.ts
|
|
1843
|
+
const { useValue: useRouterBasename, setValue: setRouterBasename, addEventHandler: addRouterBasenameEventHandler } = makePHEventFunctions("routerBasename");
|
|
1844
|
+
const { useValue: useVersion, setValue: setVersion, addEventHandler: addVersionEventHandler } = makePHEventFunctions("version");
|
|
1845
|
+
const { useValue: useRequiresHardRefresh, setValue: setRequiresHardRefresh, addEventHandler: addRequiresHardRefreshEventHandler } = makePHEventFunctions("requiresHardRefresh");
|
|
1846
|
+
const { useValue: useWarnOutdatedApp, setValue: setWarnOutdatedApp, addEventHandler: addWarnOutdatedAppEventHandler } = makePHEventFunctions("warnOutdatedApp");
|
|
1847
|
+
const { useValue: useStudioMode, setValue: setStudioMode, addEventHandler: addStudioModeEventHandler } = makePHEventFunctions("studioMode");
|
|
1848
|
+
const { useValue: useBasePath, setValue: setBasePath, addEventHandler: addBasePathEventHandler } = makePHEventFunctions("basePath");
|
|
1849
|
+
const { useValue: useVersionCheckInterval, setValue: setVersionCheckInterval, addEventHandler: addVersionCheckIntervalEventHandler } = makePHEventFunctions("versionCheckInterval");
|
|
1850
|
+
const { useValue: useCliVersion, setValue: setCliVersion, addEventHandler: addCliVersionEventHandler } = makePHEventFunctions("cliVersion");
|
|
1851
|
+
const { useValue: useFileUploadOperationsChunkSize, setValue: setFileUploadOperationsChunkSize, addEventHandler: addFileUploadOperationsChunkSizeEventHandler } = makePHEventFunctions("fileUploadOperationsChunkSize");
|
|
1852
|
+
const { useValue: useIsDocumentModelSelectionSettingsEnabled, setValue: setIsDocumentModelSelectionSettingsEnabled, addEventHandler: addIsDocumentModelSelectionSettingsEnabledEventHandler } = makePHEventFunctions("isDocumentModelSelectionSettingsEnabled");
|
|
1853
|
+
const { useValue: useGaTrackingId, setValue: setGaTrackingId, addEventHandler: addGaTrackingIdEventHandler } = makePHEventFunctions("gaTrackingId");
|
|
1854
|
+
const { useValue: useDefaultDrivesUrl, setValue: setDefaultDrivesUrl, addEventHandler: addDefaultDrivesUrlEventHandler } = makePHEventFunctions("defaultDrivesUrl");
|
|
1855
|
+
const { useValue: useDrivesPreserveStrategy, setValue: setDrivesPreserveStrategy, addEventHandler: addDrivesPreserveStrategyEventHandler } = makePHEventFunctions("drivesPreserveStrategy");
|
|
1856
|
+
const { useValue: useIsLocalDrivesEnabled, setValue: setIsLocalDrivesEnabled, addEventHandler: addIsLocalDrivesEnabledEventHandler } = makePHEventFunctions("isLocalDrivesEnabled");
|
|
1857
|
+
const { useValue: useIsAddDriveEnabled, setValue: setIsAddDriveEnabled, addEventHandler: addIsAddDriveEnabledEventHandler } = makePHEventFunctions("isAddDriveEnabled");
|
|
1858
|
+
const { useValue: useIsPublicDrivesEnabled, setValue: setIsPublicDrivesEnabled, addEventHandler: addIsPublicDrivesEnabledEventHandler } = makePHEventFunctions("isPublicDrivesEnabled");
|
|
1859
|
+
const { useValue: useIsAddPublicDrivesEnabled, setValue: setIsAddPublicDrivesEnabled, addEventHandler: addIsAddPublicDrivesEnabledEventHandler } = makePHEventFunctions("isAddPublicDrivesEnabled");
|
|
1860
|
+
const { useValue: useIsDeletePublicDrivesEnabled, setValue: setIsDeletePublicDrivesEnabled, addEventHandler: addIsDeletePublicDrivesEnabledEventHandler } = makePHEventFunctions("isDeletePublicDrivesEnabled");
|
|
1861
|
+
const { useValue: useIsCloudDrivesEnabled, setValue: setIsCloudDrivesEnabled, addEventHandler: addIsCloudDrivesEnabledEventHandler } = makePHEventFunctions("isCloudDrivesEnabled");
|
|
1862
|
+
const { useValue: useIsAddCloudDrivesEnabled, setValue: setIsAddCloudDrivesEnabled, addEventHandler: addIsAddCloudDrivesEnabledEventHandler } = makePHEventFunctions("isAddCloudDrivesEnabled");
|
|
1863
|
+
const { useValue: useIsDeleteCloudDrivesEnabled, setValue: setIsDeleteCloudDrivesEnabled, addEventHandler: addIsDeleteCloudDrivesEnabledEventHandler } = makePHEventFunctions("isDeleteCloudDrivesEnabled");
|
|
1864
|
+
const { useValue: useLocalDrivesEnabled, setValue: setLocalDrivesEnabled, addEventHandler: addLocalDrivesEnabledEventHandler } = makePHEventFunctions("isLocalDrivesEnabled");
|
|
1865
|
+
const { useValue: useIsAddLocalDrivesEnabled, setValue: setIsAddLocalDrivesEnabled, addEventHandler: addIsAddLocalDrivesEnabledEventHandler } = makePHEventFunctions("isAddLocalDrivesEnabled");
|
|
1866
|
+
const { useValue: useIsDeleteLocalDrivesEnabled, setValue: setIsDeleteLocalDrivesEnabled, addEventHandler: addIsDeleteLocalDrivesEnabledEventHandler } = makePHEventFunctions("isDeleteLocalDrivesEnabled");
|
|
1867
|
+
const { useValue: useIsEditorDebugModeEnabled, setValue: setIsEditorDebugModeEnabled, addEventHandler: addIsEditorDebugModeEnabledEventHandler } = makePHEventFunctions("isEditorDebugModeEnabled");
|
|
1868
|
+
const { useValue: useIsEditorReadModeEnabled, setValue: setIsEditorReadModeEnabled, addEventHandler: addIsEditorReadModeEnabledEventHandler } = makePHEventFunctions("isEditorReadModeEnabled");
|
|
1869
|
+
const { useValue: useIsAnalyticsDatabaseWorkerEnabled, setValue: setIsAnalyticsDatabaseWorkerEnabled, addEventHandler: addIsAnalyticsDatabaseWorkerEnabledEventHandler } = makePHEventFunctions("isAnalyticsDatabaseWorkerEnabled");
|
|
1870
|
+
const { useValue: useIsDiffAnalyticsEnabled, setValue: setIsDiffAnalyticsEnabled, addEventHandler: addIsDiffAnalyticsEnabledEventHandler } = makePHEventFunctions("isDiffAnalyticsEnabled");
|
|
1871
|
+
const { useValue: useIsDriveAnalyticsEnabled, setValue: setIsDriveAnalyticsEnabled, addEventHandler: addIsDriveAnalyticsEnabledEventHandler } = makePHEventFunctions("isDriveAnalyticsEnabled");
|
|
1872
|
+
const { useValue: useRenownUrl, setValue: setRenownUrl, addEventHandler: addRenownUrlEventHandler } = makePHEventFunctions("renownUrl");
|
|
1873
|
+
const { useValue: useRenownNetworkId, setValue: setRenownNetworkId, addEventHandler: addRenownNetworkIdEventHandler } = makePHEventFunctions("renownNetworkId");
|
|
1874
|
+
const { useValue: useRenownChainId, setValue: setRenownChainId, addEventHandler: addRenownChainIdEventHandler } = makePHEventFunctions("renownChainId");
|
|
1875
|
+
const { useValue: useSwitchboardUrl, setValue: setSwitchboardUrl, addEventHandler: addSwitchboardUrlEventHandler } = makePHEventFunctions("switchboardUrl");
|
|
1876
|
+
const { useValue: useRenownAdapters, setValue: setRenownAdapters, addEventHandler: addRenownAdaptersEventHandler } = makePHEventFunctions("renownAdapters");
|
|
1877
|
+
const { useValue: useSentryRelease, setValue: setSentryRelease, addEventHandler: addSentryReleaseEventHandler } = makePHEventFunctions("sentryRelease");
|
|
1878
|
+
const { useValue: useSentryDsn, setValue: setSentryDsn, addEventHandler: addSentryDsnEventHandler } = makePHEventFunctions("sentryDsn");
|
|
1879
|
+
const { useValue: useSentryEnv, setValue: setSentryEnv, addEventHandler: addSentryEnvEventHandler } = makePHEventFunctions("sentryEnv");
|
|
1880
|
+
const { useValue: useIsSentryTracingEnabled, setValue: setIsSentryTracingEnabled, addEventHandler: addIsSentryTracingEnabledEventHandler } = makePHEventFunctions("isSentryTracingEnabled");
|
|
1881
|
+
const { useValue: useIsExternalProcessorsEnabled, setValue: setIsExternalProcessorsEnabled, addEventHandler: addIsExternalProcessorsEnabledEventHandler } = makePHEventFunctions("isExternalProcessorsEnabled");
|
|
1882
|
+
const { useValue: useIsExternalPackagesEnabled, setValue: setIsExternalPackagesEnabled, addEventHandler: addIsExternalPackagesEnabledEventHandler } = makePHEventFunctions("isExternalPackagesEnabled");
|
|
1883
|
+
const enabledEditorsEventFunctions = makePHEventFunctions("enabledEditors");
|
|
1884
|
+
/** Sets the enabled editors for Connect. */
|
|
1885
|
+
const setEnabledEditors = enabledEditorsEventFunctions.setValue;
|
|
1886
|
+
/** Gets the enabled editors for Connect. */
|
|
1887
|
+
const useEnabledEditors = enabledEditorsEventFunctions.useValue;
|
|
1888
|
+
/** Adds an event handler for when the enabled editors for Connect changes. */
|
|
1889
|
+
const addEnabledEditorsEventHandler = enabledEditorsEventFunctions.addEventHandler;
|
|
1890
|
+
const disabledEditorsEventFunctions = makePHEventFunctions("disabledEditors");
|
|
1891
|
+
/** Sets the disabled editors for Connect. */
|
|
1892
|
+
const setDisabledEditors = disabledEditorsEventFunctions.setValue;
|
|
1893
|
+
/** Gets the disabled editors for Connect. */
|
|
1894
|
+
const useDisabledEditors = disabledEditorsEventFunctions.useValue;
|
|
1895
|
+
/** Adds an event handler for when the disabled editors for Connect changes. */
|
|
1896
|
+
const addDisabledEditorsEventHandler = disabledEditorsEventFunctions.addEventHandler;
|
|
1897
|
+
const isRelationalProcessorsEnabled = makePHEventFunctions("isRelationalProcessorsEnabled");
|
|
1898
|
+
/** Sets the isRelationalProcessorsEnabled for Connect. */
|
|
1899
|
+
const setIsRelationalProcessorsEnabled = isRelationalProcessorsEnabled.setValue;
|
|
1900
|
+
/** Gets the isRelationalProcessorsEnabled for Connect. */
|
|
1901
|
+
const useIsRelationalProcessorsEnabled = isRelationalProcessorsEnabled.useValue;
|
|
1902
|
+
/** Adds an event handler for when the isRelationalProcessorsEnabled for Connect changes. */
|
|
1903
|
+
const addIsRelationalProcessorsEnabledEventHandler = isRelationalProcessorsEnabled.addEventHandler;
|
|
1904
|
+
const isExternalRelationalProcessorsEnabled = makePHEventFunctions("isExternalRelationalProcessorsEnabled");
|
|
1905
|
+
/** Sets the isExternalRelationalProcessorsEnabled for Connect. */
|
|
1906
|
+
const setIsExternalRelationalProcessorsEnabled = isExternalRelationalProcessorsEnabled.setValue;
|
|
1907
|
+
/** Gets the isExternalRelationalProcessorsEnabled for Connect. */
|
|
1908
|
+
const useIsExternalRelationalProcessorsEnabled = isExternalRelationalProcessorsEnabled.useValue;
|
|
1909
|
+
/** Adds an event handler for when the isExternalRelationalProcessorsEnabled for Connect changes. */
|
|
1910
|
+
const addIsExternalRelationalProcessorsEnabledEventHandler = isExternalRelationalProcessorsEnabled.addEventHandler;
|
|
1911
|
+
const isAnalyticsEnabledEventFunctions = makePHEventFunctions("isAnalyticsEnabled");
|
|
1912
|
+
/** Sets the isAnalyticsEnabled for Connect. */
|
|
1913
|
+
const setIsAnalyticsEnabled = isAnalyticsEnabledEventFunctions.setValue;
|
|
1914
|
+
/** Gets the isAnalyticsEnabled for Connect. */
|
|
1915
|
+
const useIsAnalyticsEnabled = isAnalyticsEnabledEventFunctions.useValue;
|
|
1916
|
+
/** Adds an event handler for when the isAnalyticsEnabled for Connect changes. */
|
|
1917
|
+
const addIsAnalyticsEnabledEventHandler = isAnalyticsEnabledEventFunctions.addEventHandler;
|
|
1918
|
+
const isAnalyticsExternalProcessorsEnabled = makePHEventFunctions("isAnalyticsExternalProcessorsEnabled");
|
|
1919
|
+
/** Sets the isAnalyticsExternalProcessorsEnabled for Connect. */
|
|
1920
|
+
const setIsAnalyticsExternalProcessorsEnabled = isAnalyticsExternalProcessorsEnabled.setValue;
|
|
1921
|
+
/** Gets the isAnalyticsExternalProcessorsEnabled for Connect. */
|
|
1922
|
+
const useIsAnalyticsExternalProcessorsEnabled = isAnalyticsExternalProcessorsEnabled.useValue;
|
|
1923
|
+
/** Adds an event handler for when the isAnalyticsExternalProcessorsEnabled for Connect changes. */
|
|
1924
|
+
const addIsAnalyticsExternalProcessorsEnabledEventHandler = isAnalyticsExternalProcessorsEnabled.addEventHandler;
|
|
1925
|
+
const analyticsDatabaseNameEventFunctions = makePHEventFunctions("analyticsDatabaseName");
|
|
1926
|
+
/** Sets the analytics database name for Connect. */
|
|
1927
|
+
const setAnalyticsDatabaseName = analyticsDatabaseNameEventFunctions.setValue;
|
|
1928
|
+
/** Gets the analytics database name for Connect. */
|
|
1929
|
+
const useAnalyticsDatabaseName = analyticsDatabaseNameEventFunctions.useValue;
|
|
1930
|
+
/** Adds an event handler for when the analytics database name for Connect changes. */
|
|
1931
|
+
const addAnalyticsDatabaseNameEventHandler = analyticsDatabaseNameEventFunctions.addEventHandler;
|
|
1932
|
+
const logLevelEventFunctions = makePHEventFunctions("logLevel");
|
|
1933
|
+
/** Sets the log level for Connect. */
|
|
1934
|
+
const setLogLevel = logLevelEventFunctions.setValue;
|
|
1935
|
+
/** Gets the log level for Connect. */
|
|
1936
|
+
const useLogLevel = logLevelEventFunctions.useValue;
|
|
1937
|
+
/** Adds an event handler for when the log level for Connect changes. */
|
|
1938
|
+
const addLogLevelEventHandler = logLevelEventFunctions.addEventHandler;
|
|
1939
|
+
const allowListEventFunctions = makePHEventFunctions("allowList");
|
|
1940
|
+
/** Sets the allow list for Connect. */
|
|
1941
|
+
const setAllowList = allowListEventFunctions.setValue;
|
|
1942
|
+
/** Gets the allow list for Connect. */
|
|
1943
|
+
const useAllowList = allowListEventFunctions.useValue;
|
|
1944
|
+
/** Adds an event handler for when the allow list for Connect changes. */
|
|
1945
|
+
const addAllowListEventHandler = allowListEventFunctions.addEventHandler;
|
|
1946
|
+
const nonUserConfigSetters = {
|
|
1947
|
+
routerBasename: setRouterBasename,
|
|
1948
|
+
version: setVersion,
|
|
1949
|
+
requiresHardRefresh: setRequiresHardRefresh,
|
|
1950
|
+
warnOutdatedApp: setWarnOutdatedApp,
|
|
1951
|
+
studioMode: setStudioMode,
|
|
1952
|
+
basePath: setBasePath,
|
|
1953
|
+
versionCheckInterval: setVersionCheckInterval,
|
|
1954
|
+
cliVersion: setCliVersion,
|
|
1955
|
+
fileUploadOperationsChunkSize: setFileUploadOperationsChunkSize,
|
|
1956
|
+
isDocumentModelSelectionSettingsEnabled: setIsDocumentModelSelectionSettingsEnabled,
|
|
1957
|
+
gaTrackingId: setGaTrackingId,
|
|
1958
|
+
defaultDrivesUrl: setDefaultDrivesUrl,
|
|
1959
|
+
drivesPreserveStrategy: setDrivesPreserveStrategy,
|
|
1960
|
+
isLocalDrivesEnabled: setIsLocalDrivesEnabled,
|
|
1961
|
+
isAddDriveEnabled: setIsAddDriveEnabled,
|
|
1962
|
+
isPublicDrivesEnabled: setIsPublicDrivesEnabled,
|
|
1963
|
+
isAddPublicDrivesEnabled: setIsAddPublicDrivesEnabled,
|
|
1964
|
+
isDeletePublicDrivesEnabled: setIsDeletePublicDrivesEnabled,
|
|
1965
|
+
isCloudDrivesEnabled: setIsCloudDrivesEnabled,
|
|
1966
|
+
isAddCloudDrivesEnabled: setIsAddCloudDrivesEnabled,
|
|
1967
|
+
isDeleteCloudDrivesEnabled: setIsDeleteCloudDrivesEnabled,
|
|
1968
|
+
isAddLocalDrivesEnabled: setIsAddLocalDrivesEnabled,
|
|
1969
|
+
isDeleteLocalDrivesEnabled: setIsDeleteLocalDrivesEnabled,
|
|
1970
|
+
isEditorDebugModeEnabled: setIsEditorDebugModeEnabled,
|
|
1971
|
+
isEditorReadModeEnabled: setIsEditorReadModeEnabled,
|
|
1972
|
+
isRelationalProcessorsEnabled: setIsRelationalProcessorsEnabled,
|
|
1973
|
+
isExternalRelationalProcessorsEnabled: setIsExternalRelationalProcessorsEnabled,
|
|
1974
|
+
isAnalyticsEnabled: setIsAnalyticsEnabled,
|
|
1975
|
+
analyticsDatabaseName: setAnalyticsDatabaseName,
|
|
1976
|
+
isAnalyticsExternalProcessorsEnabled: setIsAnalyticsExternalProcessorsEnabled,
|
|
1977
|
+
isAnalyticsDatabaseWorkerEnabled: setIsAnalyticsDatabaseWorkerEnabled,
|
|
1978
|
+
isDiffAnalyticsEnabled: setIsDiffAnalyticsEnabled,
|
|
1979
|
+
isDriveAnalyticsEnabled: setIsDriveAnalyticsEnabled,
|
|
1980
|
+
renownUrl: setRenownUrl,
|
|
1981
|
+
renownNetworkId: setRenownNetworkId,
|
|
1982
|
+
renownChainId: setRenownChainId,
|
|
1983
|
+
switchboardUrl: setSwitchboardUrl,
|
|
1984
|
+
renownAdapters: setRenownAdapters,
|
|
1985
|
+
sentryRelease: setSentryRelease,
|
|
1986
|
+
sentryDsn: setSentryDsn,
|
|
1987
|
+
sentryEnv: setSentryEnv,
|
|
1988
|
+
isSentryTracingEnabled: setIsSentryTracingEnabled,
|
|
1989
|
+
isExternalProcessorsEnabled: setIsExternalProcessorsEnabled,
|
|
1990
|
+
isExternalPackagesEnabled: setIsExternalPackagesEnabled,
|
|
1991
|
+
allowList: setAllowList,
|
|
1992
|
+
logLevel: setLogLevel,
|
|
1993
|
+
disabledEditors: setDisabledEditors,
|
|
1994
|
+
enabledEditors: setEnabledEditors
|
|
1995
|
+
};
|
|
1996
|
+
const phGlobalConfigSetters = {
|
|
1997
|
+
...phAppConfigSetters,
|
|
1998
|
+
...phDocumentEditorConfigSetters,
|
|
1999
|
+
...nonUserConfigSetters
|
|
2000
|
+
};
|
|
2001
|
+
const nonUserConfigHooks = {
|
|
2002
|
+
routerBasename: useRouterBasename,
|
|
2003
|
+
version: useVersion,
|
|
2004
|
+
requiresHardRefresh: useRequiresHardRefresh,
|
|
2005
|
+
warnOutdatedApp: useWarnOutdatedApp,
|
|
2006
|
+
studioMode: useStudioMode,
|
|
2007
|
+
basePath: useBasePath,
|
|
2008
|
+
versionCheckInterval: useVersionCheckInterval,
|
|
2009
|
+
cliVersion: useCliVersion,
|
|
2010
|
+
fileUploadOperationsChunkSize: useFileUploadOperationsChunkSize,
|
|
2011
|
+
isDocumentModelSelectionSettingsEnabled: useIsDocumentModelSelectionSettingsEnabled,
|
|
2012
|
+
gaTrackingId: useGaTrackingId,
|
|
2013
|
+
defaultDrivesUrl: useDefaultDrivesUrl,
|
|
2014
|
+
drivesPreserveStrategy: useDrivesPreserveStrategy,
|
|
2015
|
+
isAddDriveEnabled: useIsAddDriveEnabled,
|
|
2016
|
+
isPublicDrivesEnabled: useIsPublicDrivesEnabled,
|
|
2017
|
+
isAddPublicDrivesEnabled: useIsAddPublicDrivesEnabled,
|
|
2018
|
+
isDeletePublicDrivesEnabled: useIsDeletePublicDrivesEnabled,
|
|
2019
|
+
isCloudDrivesEnabled: useIsCloudDrivesEnabled,
|
|
2020
|
+
isAddCloudDrivesEnabled: useIsAddCloudDrivesEnabled,
|
|
2021
|
+
isDeleteCloudDrivesEnabled: useIsDeleteCloudDrivesEnabled,
|
|
2022
|
+
isLocalDrivesEnabled: useIsLocalDrivesEnabled,
|
|
2023
|
+
isAddLocalDrivesEnabled: useIsAddLocalDrivesEnabled,
|
|
2024
|
+
isDeleteLocalDrivesEnabled: useIsDeleteLocalDrivesEnabled,
|
|
2025
|
+
isEditorDebugModeEnabled: useIsEditorDebugModeEnabled,
|
|
2026
|
+
isEditorReadModeEnabled: useIsEditorReadModeEnabled,
|
|
2027
|
+
isAnalyticsDatabaseWorkerEnabled: useIsAnalyticsDatabaseWorkerEnabled,
|
|
2028
|
+
isDiffAnalyticsEnabled: useIsDiffAnalyticsEnabled,
|
|
2029
|
+
isDriveAnalyticsEnabled: useIsDriveAnalyticsEnabled,
|
|
2030
|
+
renownUrl: useRenownUrl,
|
|
2031
|
+
renownNetworkId: useRenownNetworkId,
|
|
2032
|
+
renownChainId: useRenownChainId,
|
|
2033
|
+
switchboardUrl: useSwitchboardUrl,
|
|
2034
|
+
renownAdapters: useRenownAdapters,
|
|
2035
|
+
sentryRelease: useSentryRelease,
|
|
2036
|
+
sentryDsn: useSentryDsn,
|
|
2037
|
+
sentryEnv: useSentryEnv,
|
|
2038
|
+
isSentryTracingEnabled: useIsSentryTracingEnabled,
|
|
2039
|
+
isExternalProcessorsEnabled: useIsExternalProcessorsEnabled,
|
|
2040
|
+
isExternalPackagesEnabled: useIsExternalPackagesEnabled,
|
|
2041
|
+
allowList: useAllowList,
|
|
2042
|
+
isAnalyticsEnabled: useIsAnalyticsEnabled,
|
|
2043
|
+
isAnalyticsExternalProcessorsEnabled: useIsAnalyticsExternalProcessorsEnabled,
|
|
2044
|
+
isRelationalProcessorsEnabled: useIsRelationalProcessorsEnabled,
|
|
2045
|
+
isExternalRelationalProcessorsEnabled: useIsExternalRelationalProcessorsEnabled,
|
|
2046
|
+
analyticsDatabaseName: useAnalyticsDatabaseName,
|
|
2047
|
+
logLevel: useLogLevel,
|
|
2048
|
+
disabledEditors: useDisabledEditors,
|
|
2049
|
+
enabledEditors: useEnabledEditors
|
|
2050
|
+
};
|
|
2051
|
+
const phGlobalConfigHooks = {
|
|
2052
|
+
...phAppConfigHooks,
|
|
2053
|
+
...phDocumentEditorConfigHooks,
|
|
2054
|
+
...nonUserConfigHooks
|
|
2055
|
+
};
|
|
2056
|
+
//#endregion
|
|
2057
|
+
//#region src/hooks/attachment-service.ts
|
|
2058
|
+
const attachmentServiceEventFunctions = makePHEventFunctions("attachmentService");
|
|
2059
|
+
/** Returns the attachment service from window.ph */
|
|
2060
|
+
const useAttachmentService = attachmentServiceEventFunctions.useValue;
|
|
2061
|
+
/** Sets the attachment service on window.ph */
|
|
2062
|
+
const setAttachmentService = attachmentServiceEventFunctions.setValue;
|
|
2063
|
+
/** Registers the attachmentService window event handler */
|
|
2064
|
+
const addAttachmentServiceEventHandler = attachmentServiceEventFunctions.addEventHandler;
|
|
2065
|
+
//#endregion
|
|
2066
|
+
//#region src/hooks/features.ts
|
|
2067
|
+
const featuresEventFunctions = makePHEventFunctions("features");
|
|
2068
|
+
const useFeatures = featuresEventFunctions.useValue;
|
|
2069
|
+
const setFeatures = featuresEventFunctions.setValue;
|
|
2070
|
+
const addFeaturesEventHandler = featuresEventFunctions.addEventHandler;
|
|
2071
|
+
//#endregion
|
|
2072
|
+
//#region src/hooks/use-drop-target.ts
|
|
2073
|
+
function useDropTarget() {
|
|
2074
|
+
const [isDropTarget, setIsDropTarget] = useState(false);
|
|
2075
|
+
const isDragAndDropEnabled = useIsDragAndDropEnabled();
|
|
2076
|
+
const targetSetter = funnel(() => setIsDropTarget(true), {
|
|
2077
|
+
triggerAt: "start",
|
|
2078
|
+
minQuietPeriodMs: 100
|
|
2079
|
+
});
|
|
2080
|
+
const targetUnsetter = funnel(() => setIsDropTarget(false), {
|
|
2081
|
+
triggerAt: "start",
|
|
2082
|
+
minQuietPeriodMs: 100
|
|
2083
|
+
});
|
|
2084
|
+
function setTarget() {
|
|
2085
|
+
if (!isDragAndDropEnabled) return;
|
|
2086
|
+
targetUnsetter.cancel();
|
|
2087
|
+
targetSetter.call();
|
|
2088
|
+
}
|
|
2089
|
+
function unsetTarget() {
|
|
2090
|
+
if (!isDragAndDropEnabled) return;
|
|
2091
|
+
targetSetter.cancel();
|
|
2092
|
+
targetUnsetter.call();
|
|
2093
|
+
}
|
|
2094
|
+
return {
|
|
2095
|
+
isDropTarget,
|
|
2096
|
+
setTarget,
|
|
2097
|
+
unsetTarget
|
|
2098
|
+
};
|
|
2099
|
+
}
|
|
2100
|
+
//#endregion
|
|
2101
|
+
//#region src/hooks/node-drag-and-drop.ts
|
|
2102
|
+
const draggingNodeEventFunctions = makePHEventFunctions("draggingNode");
|
|
2103
|
+
const useDraggingNode = draggingNodeEventFunctions.useValue;
|
|
2104
|
+
const setDraggingNode = draggingNodeEventFunctions.setValue;
|
|
2105
|
+
const addDraggingNodeEventHandler = draggingNodeEventFunctions.addEventHandler;
|
|
2106
|
+
const draggingNodeSetter = funnel((node) => setDraggingNode(node), {
|
|
2107
|
+
reducer: (_, newNode) => newNode,
|
|
2108
|
+
triggerAt: "start",
|
|
2109
|
+
minQuietPeriodMs: 100
|
|
2110
|
+
});
|
|
2111
|
+
const draggingNodeUnsetter = funnel(() => setDraggingNode(void 0), {
|
|
2112
|
+
triggerAt: "start",
|
|
2113
|
+
minQuietPeriodMs: 100
|
|
2114
|
+
});
|
|
2115
|
+
function setDragging(node) {
|
|
2116
|
+
draggingNodeUnsetter.cancel();
|
|
2117
|
+
draggingNodeSetter.call(node);
|
|
2118
|
+
}
|
|
2119
|
+
function unsetDragging() {
|
|
2120
|
+
draggingNodeSetter.cancel();
|
|
2121
|
+
draggingNodeUnsetter.call();
|
|
2122
|
+
}
|
|
2123
|
+
const isNodeDrag = (params) => conditional(params, [({ srcId }) => isNot(isTruthy)(srcId), constant(false)], [({ driveId }) => isNot(isTruthy)(driveId), constant(false)], [({ driveId, srcId }) => isStrictEqual(driveId, srcId), constant(false)], constant(true));
|
|
2124
|
+
function useDragNode(args) {
|
|
2125
|
+
const { srcId, parentId } = args;
|
|
2126
|
+
const driveId = useSelectedDriveId();
|
|
2127
|
+
const draggingNodeId = useDraggingNode()?.srcId;
|
|
2128
|
+
const params = {
|
|
2129
|
+
driveId,
|
|
2130
|
+
srcId,
|
|
2131
|
+
parentId
|
|
2132
|
+
};
|
|
2133
|
+
const draggable = isNodeDrag(params);
|
|
2134
|
+
const isDragging = allPass({
|
|
2135
|
+
srcId,
|
|
2136
|
+
draggingNodeId
|
|
2137
|
+
}, [
|
|
2138
|
+
() => draggable,
|
|
2139
|
+
({ srcId }) => isString(srcId),
|
|
2140
|
+
({ draggingNodeId }) => isString(draggingNodeId),
|
|
2141
|
+
({ srcId, draggingNodeId }) => isStrictEqual(srcId, draggingNodeId)
|
|
2142
|
+
]);
|
|
2143
|
+
const onDragStart = () => {
|
|
2144
|
+
if (!draggable) return;
|
|
2145
|
+
setDragging(params);
|
|
2146
|
+
};
|
|
2147
|
+
const onDragEnd = () => {
|
|
2148
|
+
if (!draggable) return;
|
|
2149
|
+
unsetDragging();
|
|
2150
|
+
};
|
|
2151
|
+
return {
|
|
2152
|
+
isDragging,
|
|
2153
|
+
draggable,
|
|
2154
|
+
onDragStart,
|
|
2155
|
+
onDragEnd
|
|
2156
|
+
};
|
|
2157
|
+
}
|
|
2158
|
+
const isNodeDrop = (params) => conditional(params, [({ srcId }) => isNot(isTruthy)(srcId), constant(false)], [({ driveId }) => isNot(isDefined)(driveId), constant(false)], [({ srcId, targetId }) => isStrictEqual(srcId, targetId), constant(false)], [({ driveId, parentId, targetId }) => isNot(isTruthy)(parentId) && isStrictEqual(targetId, driveId), constant(false)], [({ targetId, parentId }) => isStrictEqual(targetId, parentId), constant(false)], constant(true));
|
|
2159
|
+
function useDropNode(targetId) {
|
|
2160
|
+
const driveId = useSelectedDriveId();
|
|
2161
|
+
const { srcId, parentId } = useDraggingNode() ?? {};
|
|
2162
|
+
const { isDropTarget, setTarget, unsetTarget } = useDropTarget();
|
|
2163
|
+
const params = {
|
|
2164
|
+
driveId,
|
|
2165
|
+
parentId,
|
|
2166
|
+
targetId,
|
|
2167
|
+
srcId
|
|
2168
|
+
};
|
|
2169
|
+
function handleNodeDrop(event, cb) {
|
|
2170
|
+
if (!isNodeDrop(params)) return;
|
|
2171
|
+
event.preventDefault();
|
|
2172
|
+
event.stopPropagation();
|
|
2173
|
+
cb?.();
|
|
2174
|
+
}
|
|
2175
|
+
const onDragEnter = (event) => handleNodeDrop(event);
|
|
2176
|
+
const onDragOver = (event) => handleNodeDrop(event, once(setTarget));
|
|
2177
|
+
const onDragLeave = (event) => handleNodeDrop(event, once(unsetTarget));
|
|
2178
|
+
const onDrop = (event) => handleNodeDrop(event, once(() => {
|
|
2179
|
+
unsetDragging();
|
|
2180
|
+
unsetTarget();
|
|
2181
|
+
moveNodeById(params).catch(console.error);
|
|
2182
|
+
}));
|
|
2183
|
+
return {
|
|
2184
|
+
isDropTarget,
|
|
2185
|
+
onDragEnter,
|
|
2186
|
+
onDragOver,
|
|
2187
|
+
onDragLeave,
|
|
2188
|
+
onDrop
|
|
2189
|
+
};
|
|
2190
|
+
}
|
|
2191
|
+
//#endregion
|
|
2192
|
+
//#region src/hooks/graphql-reactor-client.ts
|
|
2193
|
+
const graphQLReactorClientEventFunctions = makePHEventFunctions("reactorGraphQLClient");
|
|
2194
|
+
const useGraphQLReactorClient = graphQLReactorClientEventFunctions.useValue;
|
|
2195
|
+
const setGraphQLReactorClient = graphQLReactorClientEventFunctions.setValue;
|
|
2196
|
+
const addGraphQLReactorClientEventHandler = graphQLReactorClientEventFunctions.addEventHandler;
|
|
2197
|
+
//#endregion
|
|
2198
|
+
//#region src/hooks/package-discovery.ts
|
|
2199
|
+
const packageDiscoveryFunctions = makePHEventFunctions("packageDiscoveryService");
|
|
2200
|
+
const usePackageDiscoveryService = packageDiscoveryFunctions.useValue;
|
|
2201
|
+
const setPackageDiscoveryService = packageDiscoveryFunctions.setValue;
|
|
2202
|
+
const addPackageDiscoveryServiceEventHandler = packageDiscoveryFunctions.addEventHandler;
|
|
2203
|
+
//#endregion
|
|
2204
|
+
//#region src/hooks/reactor.ts
|
|
2205
|
+
const reactorClientModuleEventFunctions = makePHEventFunctions("reactorClientModule");
|
|
2206
|
+
const reactorClientEventFunctions = makePHEventFunctions("reactorClient");
|
|
2207
|
+
/** Returns the reactor client module (in-process or worker-backed) */
|
|
2208
|
+
const useReactorClientModule = reactorClientModuleEventFunctions.useValue;
|
|
2209
|
+
/** Sets the reactor client module */
|
|
2210
|
+
const setReactorClientModule = reactorClientModuleEventFunctions.setValue;
|
|
2211
|
+
/** Adds an event handler for the reactor client module */
|
|
2212
|
+
const addReactorClientModuleEventHandler = reactorClientModuleEventFunctions.addEventHandler;
|
|
2213
|
+
/** Returns the reactor client */
|
|
2214
|
+
const useReactorClient = reactorClientEventFunctions.useValue;
|
|
2215
|
+
/** Sets the reactor client */
|
|
2216
|
+
const setReactorClient = reactorClientEventFunctions.setValue;
|
|
2217
|
+
/** Adds an event handler for the reactor client */
|
|
2218
|
+
const addReactorClientEventHandler = reactorClientEventFunctions.addEventHandler;
|
|
2219
|
+
const useSync = () => useReactorClientModule()?.reactorModule?.syncModule?.syncManager;
|
|
2220
|
+
const useSyncList = () => {
|
|
2221
|
+
return useSync()?.list() ?? [];
|
|
2222
|
+
};
|
|
2223
|
+
const useModelRegistry = () => useReactorClientModule()?.reactorModule?.documentModelRegistry;
|
|
2224
|
+
const useDatabase = () => {
|
|
2225
|
+
const module = useReactorClientModule();
|
|
2226
|
+
return module?.kind === "browser" ? module.reactorModule?.database : void 0;
|
|
2227
|
+
};
|
|
2228
|
+
const usePGlite = () => {
|
|
2229
|
+
const module = useReactorClientModule();
|
|
2230
|
+
return module?.kind === "browser" ? module.reactorModule?.pg : void 0;
|
|
2231
|
+
};
|
|
2232
|
+
//#endregion
|
|
2233
|
+
//#region src/hooks/revision-history.ts
|
|
2234
|
+
const revisionHistoryEventFunctions = makePHEventFunctions("revisionHistoryVisible");
|
|
2235
|
+
/** Returns whether revision history is visible */
|
|
2236
|
+
const useRevisionHistoryVisible = revisionHistoryEventFunctions.useValue;
|
|
2237
|
+
/** Sets revision history visibility */
|
|
2238
|
+
const setRevisionHistoryVisible = revisionHistoryEventFunctions.setValue;
|
|
2239
|
+
/** Adds event handler for revision history visibility */
|
|
2240
|
+
const addRevisionHistoryVisibleEventHandler = revisionHistoryEventFunctions.addEventHandler;
|
|
2241
|
+
/** Shows the revision history */
|
|
2242
|
+
function showRevisionHistory() {
|
|
2243
|
+
setRevisionHistoryVisible(true);
|
|
2244
|
+
}
|
|
2245
|
+
/** Hides the revision history */
|
|
2246
|
+
function hideRevisionHistory() {
|
|
2247
|
+
setRevisionHistoryVisible(false);
|
|
2248
|
+
}
|
|
2249
|
+
//#endregion
|
|
2250
|
+
//#region src/hooks/selected-timeline-item.ts
|
|
2251
|
+
const selectedTimelineItemEventFunctions = makePHEventFunctions("selectedTimelineItem");
|
|
2252
|
+
/** Returns the selected timeline item */
|
|
2253
|
+
const useSelectedTimelineItem = selectedTimelineItemEventFunctions.useValue;
|
|
2254
|
+
/** Sets the selected timeline item */
|
|
2255
|
+
const setSelectedTimelineItem = selectedTimelineItemEventFunctions.setValue;
|
|
2256
|
+
/** Adds event handler for selected timeline item */
|
|
2257
|
+
const addSelectedTimelineItemEventHandler = selectedTimelineItemEventFunctions.addEventHandler;
|
|
2258
|
+
//#endregion
|
|
2259
|
+
//#region src/hooks/timeline-revision.ts
|
|
2260
|
+
const selectedTimelineRevisionEventFunctions = makePHEventFunctions("selectedTimelineRevision");
|
|
2261
|
+
/** Returns the selected timeline revision. */
|
|
2262
|
+
const useSelectedTimelineRevision = selectedTimelineRevisionEventFunctions.useValue;
|
|
2263
|
+
/** Sets the selected timeline revision. */
|
|
2264
|
+
const setSelectedTimelineRevision = selectedTimelineRevisionEventFunctions.setValue;
|
|
2265
|
+
/** Adds an event handler for the selected timeline revision. */
|
|
2266
|
+
const addSelectedTimelineRevisionEventHandler = selectedTimelineRevisionEventFunctions.addEventHandler;
|
|
2267
|
+
//#endregion
|
|
2268
|
+
//#region src/hooks/toast.ts
|
|
2269
|
+
const toastEventFunctions = makePHEventFunctions("toast");
|
|
2270
|
+
/** Returns the toast function */
|
|
2271
|
+
const usePHToast = toastEventFunctions.useValue;
|
|
2272
|
+
/** Sets the toast function */
|
|
2273
|
+
const setPHToast = toastEventFunctions.setValue;
|
|
2274
|
+
/** Adds an event handler for the toast */
|
|
2275
|
+
const addToastEventHandler = toastEventFunctions.addEventHandler;
|
|
2276
|
+
//#endregion
|
|
2277
|
+
//#region src/hooks/add-ph-event-handlers.ts
|
|
2278
|
+
const commonGlobalEventHandlerFunctions = {
|
|
2279
|
+
loading: addLoadingEventHandler,
|
|
2280
|
+
drives: addDrivesEventHandler,
|
|
2281
|
+
selectedDriveId: () => {
|
|
2282
|
+
addSelectedDriveIdEventHandler();
|
|
2283
|
+
addSetSelectedDriveOnPopStateEventHandler();
|
|
2284
|
+
addResetSelectedNodeEventHandler();
|
|
2285
|
+
},
|
|
2286
|
+
selectedNodeId: () => {
|
|
2287
|
+
addSelectedNodeIdEventHandler();
|
|
2288
|
+
addSetSelectedNodeOnPopStateEventHandler();
|
|
2289
|
+
},
|
|
2290
|
+
documentCache: addDocumentCacheEventHandler,
|
|
2291
|
+
reactorGraphQLClient: addGraphQLReactorClientEventHandler,
|
|
2292
|
+
draggingNode: addDraggingNodeEventHandler
|
|
2293
|
+
};
|
|
2294
|
+
const phGlobalEventHandlerRegisterFunctions = {
|
|
2295
|
+
...commonGlobalEventHandlerFunctions,
|
|
2296
|
+
reactorClientModule: addReactorClientModuleEventHandler,
|
|
2297
|
+
reactorClient: addReactorClientEventHandler,
|
|
2298
|
+
attachmentService: addAttachmentServiceEventHandler,
|
|
2299
|
+
features: addFeaturesEventHandler,
|
|
2300
|
+
modal: addModalEventHandler,
|
|
2301
|
+
renown: addRenownEventHandler,
|
|
2302
|
+
vetraPackageManager: addVetraPackageManagerEventHandler,
|
|
2303
|
+
packageDiscoveryService: addPackageDiscoveryServiceEventHandler,
|
|
2304
|
+
selectedTimelineRevision: addSelectedTimelineRevisionEventHandler,
|
|
2305
|
+
revisionHistoryVisible: addRevisionHistoryVisibleEventHandler,
|
|
2306
|
+
selectedTimelineItem: addSelectedTimelineItemEventHandler,
|
|
2307
|
+
routerBasename: addRouterBasenameEventHandler,
|
|
2308
|
+
version: addVersionEventHandler,
|
|
2309
|
+
logLevel: addLogLevelEventHandler,
|
|
2310
|
+
requiresHardRefresh: addRequiresHardRefreshEventHandler,
|
|
2311
|
+
warnOutdatedApp: addWarnOutdatedAppEventHandler,
|
|
2312
|
+
studioMode: addStudioModeEventHandler,
|
|
2313
|
+
basePath: addBasePathEventHandler,
|
|
2314
|
+
versionCheckInterval: addVersionCheckIntervalEventHandler,
|
|
2315
|
+
cliVersion: addCliVersionEventHandler,
|
|
2316
|
+
fileUploadOperationsChunkSize: addFileUploadOperationsChunkSizeEventHandler,
|
|
2317
|
+
isDocumentModelSelectionSettingsEnabled: addIsDocumentModelSelectionSettingsEnabledEventHandler,
|
|
2318
|
+
gaTrackingId: addGaTrackingIdEventHandler,
|
|
2319
|
+
allowList: addAllowListEventHandler,
|
|
2320
|
+
defaultDrivesUrl: addDefaultDrivesUrlEventHandler,
|
|
2321
|
+
drivesPreserveStrategy: addDrivesPreserveStrategyEventHandler,
|
|
2322
|
+
allowedDocumentTypes: addAllowedDocumentTypesEventHandler,
|
|
2323
|
+
enabledEditors: addEnabledEditorsEventHandler,
|
|
2324
|
+
disabledEditors: addDisabledEditorsEventHandler,
|
|
2325
|
+
isAddDriveEnabled: addIsAddDriveEnabledEventHandler,
|
|
2326
|
+
isLocalDrivesEnabled: addIsLocalDrivesEnabledEventHandler,
|
|
2327
|
+
isPublicDrivesEnabled: addIsPublicDrivesEnabledEventHandler,
|
|
2328
|
+
isAddPublicDrivesEnabled: addIsAddPublicDrivesEnabledEventHandler,
|
|
2329
|
+
isDeletePublicDrivesEnabled: addIsDeletePublicDrivesEnabledEventHandler,
|
|
2330
|
+
isCloudDrivesEnabled: addIsCloudDrivesEnabledEventHandler,
|
|
2331
|
+
isAddCloudDrivesEnabled: addIsAddCloudDrivesEnabledEventHandler,
|
|
2332
|
+
isDeleteCloudDrivesEnabled: addIsDeleteCloudDrivesEnabledEventHandler,
|
|
2333
|
+
isAddLocalDrivesEnabled: addIsAddLocalDrivesEnabledEventHandler,
|
|
2334
|
+
isDeleteLocalDrivesEnabled: addIsDeleteLocalDrivesEnabledEventHandler,
|
|
2335
|
+
isDragAndDropEnabled: addIsDragAndDropEnabledEventHandler,
|
|
2336
|
+
isExternalControlsEnabled: addIsExternalControlsEnabledEventHandler,
|
|
2337
|
+
isEditorDebugModeEnabled: addIsEditorDebugModeEnabledEventHandler,
|
|
2338
|
+
isEditorReadModeEnabled: addIsEditorReadModeEnabledEventHandler,
|
|
2339
|
+
isRelationalProcessorsEnabled: addIsRelationalProcessorsEnabledEventHandler,
|
|
2340
|
+
isExternalRelationalProcessorsEnabled: addIsExternalRelationalProcessorsEnabledEventHandler,
|
|
2341
|
+
isAnalyticsEnabled: addIsAnalyticsEnabledEventHandler,
|
|
2342
|
+
isAnalyticsExternalProcessorsEnabled: addIsAnalyticsExternalProcessorsEnabledEventHandler,
|
|
2343
|
+
analyticsDatabaseName: addAnalyticsDatabaseNameEventHandler,
|
|
2344
|
+
isAnalyticsDatabaseWorkerEnabled: addIsAnalyticsDatabaseWorkerEnabledEventHandler,
|
|
2345
|
+
isDiffAnalyticsEnabled: addIsDiffAnalyticsEnabledEventHandler,
|
|
2346
|
+
isDriveAnalyticsEnabled: addIsDriveAnalyticsEnabledEventHandler,
|
|
2347
|
+
renownUrl: addRenownUrlEventHandler,
|
|
2348
|
+
renownNetworkId: addRenownNetworkIdEventHandler,
|
|
2349
|
+
renownChainId: addRenownChainIdEventHandler,
|
|
2350
|
+
switchboardUrl: addSwitchboardUrlEventHandler,
|
|
2351
|
+
renownAdapters: addRenownAdaptersEventHandler,
|
|
2352
|
+
sentryRelease: addSentryReleaseEventHandler,
|
|
2353
|
+
sentryDsn: addSentryDsnEventHandler,
|
|
2354
|
+
sentryEnv: addSentryEnvEventHandler,
|
|
2355
|
+
isSentryTracingEnabled: addIsSentryTracingEnabledEventHandler,
|
|
2356
|
+
isExternalProcessorsEnabled: addIsExternalProcessorsEnabledEventHandler,
|
|
2357
|
+
isExternalPackagesEnabled: addIsExternalPackagesEnabledEventHandler,
|
|
2358
|
+
toast: addToastEventHandler
|
|
2359
|
+
};
|
|
2360
|
+
function addPHEventHandlers() {
|
|
2361
|
+
callEventHandlerRegisterFunctions(phGlobalEventHandlerRegisterFunctions);
|
|
2362
|
+
}
|
|
2363
|
+
function callEventHandlerRegisterFunctions(registerFunctions) {
|
|
2364
|
+
forEachObj(registerFunctions, (fn) => fn());
|
|
2365
|
+
}
|
|
2366
|
+
//#endregion
|
|
2367
|
+
//#region src/graphql-client/graphql-reactor-provider.tsx
|
|
2368
|
+
/**
|
|
2369
|
+
* Registers the `window.ph` event handlers once per page.
|
|
2370
|
+
*
|
|
2371
|
+
* The `set*` helpers only dispatch CustomEvents - nothing writes `window.ph`
|
|
2372
|
+
* until `addPHEventHandlers` has registered the listeners that do. Registration
|
|
2373
|
+
* is guarded by a marker on `window` rather than a module-level flag so that a
|
|
2374
|
+
* hot module replacement, or two copies of this module, still register once.
|
|
2375
|
+
*/
|
|
2376
|
+
function ensurePHEventHandlers() {
|
|
2377
|
+
if (typeof window === "undefined" || window.__phEventHandlersRegistered) return;
|
|
2378
|
+
addPHEventHandlers();
|
|
2379
|
+
window.__phEventHandlersRegistered = true;
|
|
2380
|
+
}
|
|
2381
|
+
/**
|
|
2382
|
+
* Mounts a {@link GraphQLReactorClient} into the `window.ph` slots the
|
|
2383
|
+
* reactor-browser hooks read, so `useDocument`, `useDispatch` and
|
|
2384
|
+
* `useReactorClient` work below it against a Switchboard, with no reactor in
|
|
2385
|
+
* the bundle.
|
|
2386
|
+
*
|
|
2387
|
+
* It fills the document slots, plus `window.ph.vetraPackageManager` when
|
|
2388
|
+
* `documentModels` is given (a fixed {@link StaticPackageManager}, which makes
|
|
2389
|
+
* the document-model hooks work). The same modules go to the client itself, so
|
|
2390
|
+
* a dispatch of two or more actions is signed rather than refused.
|
|
2391
|
+
* `window.ph.reactorClientModule` always stays empty, so full-reactor surfaces
|
|
2392
|
+
* (drives, jobs, editor auto-discovery) find nothing - an app below this
|
|
2393
|
+
* provider renders components it imports itself.
|
|
2394
|
+
*
|
|
2395
|
+
* The client is built on the client only: on the server the slots stay empty
|
|
2396
|
+
* and the hooks report their normal loading states. The props are read once,
|
|
2397
|
+
* when the client is built - changing `url` afterwards does not rebuild it.
|
|
2398
|
+
*
|
|
2399
|
+
* The effect's cleanup disposes the cache and the client's socket, and the
|
|
2400
|
+
* effect rebuilds both if it runs again on the same client - which is what a
|
|
2401
|
+
* `StrictMode` mount does.
|
|
2402
|
+
*/
|
|
2403
|
+
function GraphQLReactorProvider({ url, tokenProvider, subscriptionsUrl, realtime, attachmentService, documentModels, children }) {
|
|
2404
|
+
const [client] = useState(() => typeof window === "undefined" ? void 0 : new GraphQLReactorClient({
|
|
2405
|
+
url,
|
|
2406
|
+
tokenProvider,
|
|
2407
|
+
subscriptionsUrl,
|
|
2408
|
+
realtime,
|
|
2409
|
+
documentModels
|
|
2410
|
+
}));
|
|
2411
|
+
useEffect(() => {
|
|
2412
|
+
if (!client) return;
|
|
2413
|
+
ensurePHEventHandlers();
|
|
2414
|
+
setReactorClient(client);
|
|
2415
|
+
const documentCache = new DocumentCache(client);
|
|
2416
|
+
setDocumentCache(documentCache);
|
|
2417
|
+
return () => {
|
|
2418
|
+
documentCache.dispose();
|
|
2419
|
+
client.dispose();
|
|
2420
|
+
};
|
|
2421
|
+
}, [client]);
|
|
2422
|
+
useEffect(() => {
|
|
2423
|
+
if (!attachmentService) return;
|
|
2424
|
+
ensurePHEventHandlers();
|
|
2425
|
+
setAttachmentService(attachmentService);
|
|
2426
|
+
}, [attachmentService]);
|
|
2427
|
+
useEffect(() => {
|
|
2428
|
+
if (!documentModels || documentModels.length === 0) return;
|
|
2429
|
+
ensurePHEventHandlers();
|
|
2430
|
+
setVetraPackageManager(new StaticPackageManager([packageFromDocumentModels(documentModels)]));
|
|
2431
|
+
}, [documentModels]);
|
|
2432
|
+
return /* @__PURE__ */ jsx(Fragment, { children });
|
|
2433
|
+
}
|
|
2434
|
+
/**
|
|
2435
|
+
* Returns the {@link GraphQLReactorClient} mounted by
|
|
2436
|
+
* {@link GraphQLReactorProvider}, or `undefined` when the slot holds something
|
|
2437
|
+
* else - the full in-browser reactor client, or nothing yet.
|
|
2438
|
+
*
|
|
2439
|
+
* `useReactorClient` is typed as the narrow `IReactorBrowserClient` subset both
|
|
2440
|
+
* implementations share. This is how a component reaches the light client's own
|
|
2441
|
+
* surface, i.e. `subgraph` and `request`.
|
|
2442
|
+
*
|
|
2443
|
+
* The narrowing is by brand, not by `instanceof`: a page can carry two copies
|
|
2444
|
+
* of this module - a bundled package with its own copy, or a hot-replaced one -
|
|
2445
|
+
* and `instanceof` would then answer `false` for a perfectly good client and
|
|
2446
|
+
* leave the component silently without one.
|
|
2447
|
+
*/
|
|
2448
|
+
function useSwitchboardClient() {
|
|
2449
|
+
const client = useReactorClient();
|
|
2450
|
+
return isGraphQLReactorClient(client) ? client : void 0;
|
|
2451
|
+
}
|
|
2452
|
+
//#endregion
|
|
2453
|
+
//#region src/hooks/document-operations.ts
|
|
2454
|
+
/**
|
|
2455
|
+
* Hook to fetch document operations via the reactor client.
|
|
2456
|
+
* Operations are no longer auto-populated on documents and must be fetched explicitly.
|
|
2457
|
+
*
|
|
2458
|
+
* @param documentId - The document ID to fetch operations for
|
|
2459
|
+
* @returns Object containing globalOperations, localOperations, isLoading, and error
|
|
2460
|
+
*/
|
|
2461
|
+
function useDocumentOperations(documentId) {
|
|
2462
|
+
const reactorClient = useReactorClient();
|
|
2463
|
+
const hasFetchedRef = useRef(false);
|
|
2464
|
+
const [state, setState] = useState(() => ({
|
|
2465
|
+
globalOperations: [],
|
|
2466
|
+
localOperations: [],
|
|
2467
|
+
isLoading: !!documentId,
|
|
2468
|
+
error: void 0
|
|
2469
|
+
}));
|
|
2470
|
+
const fetchOperations = useCallback(async (retryCount = 0) => {
|
|
2471
|
+
const MAX_RETRIES = 5;
|
|
2472
|
+
const RETRY_DELAY_MS = 500;
|
|
2473
|
+
if (!documentId || !reactorClient) {
|
|
2474
|
+
setState({
|
|
2475
|
+
globalOperations: [],
|
|
2476
|
+
localOperations: [],
|
|
2477
|
+
isLoading: false,
|
|
2478
|
+
error: void 0
|
|
2479
|
+
});
|
|
2480
|
+
return;
|
|
2481
|
+
}
|
|
2482
|
+
setState((prev) => ({
|
|
2483
|
+
...prev,
|
|
2484
|
+
isLoading: true,
|
|
2485
|
+
error: void 0
|
|
2486
|
+
}));
|
|
2487
|
+
let globalOps = [];
|
|
2488
|
+
let localOps = [];
|
|
2489
|
+
let fetchError;
|
|
2490
|
+
try {
|
|
2491
|
+
globalOps = (await reactorClient.getOperations(documentId, { scopes: ["global"] })).results;
|
|
2492
|
+
} catch (err) {
|
|
2493
|
+
fetchError = err instanceof Error ? err : new Error(String(err));
|
|
2494
|
+
}
|
|
2495
|
+
if (!fetchError) try {
|
|
2496
|
+
localOps = (await reactorClient.getOperations(documentId, { scopes: ["local"] })).results;
|
|
2497
|
+
} catch (err) {
|
|
2498
|
+
fetchError = err instanceof Error ? err : new Error(String(err));
|
|
2499
|
+
}
|
|
2500
|
+
if (!fetchError && globalOps.length === 0 && localOps.length === 0 && retryCount < MAX_RETRIES) {
|
|
2501
|
+
await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS));
|
|
2502
|
+
return fetchOperations(retryCount + 1);
|
|
2503
|
+
}
|
|
2504
|
+
setState({
|
|
2505
|
+
globalOperations: globalOps,
|
|
2506
|
+
localOperations: localOps,
|
|
2507
|
+
isLoading: false,
|
|
2508
|
+
error: fetchError
|
|
2509
|
+
});
|
|
2510
|
+
hasFetchedRef.current = true;
|
|
2511
|
+
}, [documentId, reactorClient]);
|
|
2512
|
+
useEffect(() => {
|
|
2513
|
+
if (documentId && reactorClient) fetchOperations();
|
|
2514
|
+
else if (!documentId) {
|
|
2515
|
+
setState({
|
|
2516
|
+
globalOperations: [],
|
|
2517
|
+
localOperations: [],
|
|
2518
|
+
isLoading: false,
|
|
2519
|
+
error: void 0
|
|
2520
|
+
});
|
|
2521
|
+
hasFetchedRef.current = false;
|
|
2522
|
+
}
|
|
2523
|
+
}, [
|
|
2524
|
+
documentId,
|
|
2525
|
+
reactorClient,
|
|
2526
|
+
fetchOperations
|
|
2527
|
+
]);
|
|
2528
|
+
const refetch = useCallback(() => {
|
|
2529
|
+
fetchOperations(0);
|
|
2530
|
+
}, [fetchOperations]);
|
|
2531
|
+
return {
|
|
2532
|
+
...state,
|
|
2533
|
+
refetch
|
|
2534
|
+
};
|
|
2535
|
+
}
|
|
2536
|
+
//#endregion
|
|
2537
|
+
export { addDefaultDrivesUrlEventHandler as $, useIsAddLocalDrivesEnabled as $n, makeAuthConnectionParams as $r, setEnabledEditors as $t, useReactorClientModule as A, renameDriveNode as Ai, setRenownUrl as An, useSentryRelease as Ar, addRenownAdaptersEventHandler as At, useDragNode as B, showDeleteNodeModal as Bi, setWarnOutdatedApp as Bn, phAppConfigHooks as Br, addSwitchboardUrlEventHandler as Bt, addReactorClientModuleEventHandler as C, addFileWithProgress as Ci, setIsRelationalProcessorsEnabled as Cn, useRenownChainId as Cr, addIsExternalRelationalProcessorsEnabledEventHandler as Ct, useModelRegistry as D, exportFile as Di, setRenownAdapters as Dn, useRouterBasename as Dr, addIsSentryTracingEnabledEventHandler as Dt, useDatabase as E, deleteNode as Ei, setLogLevel as En, useRequiresHardRefresh as Er, addIsRelationalProcessorsEnabledEventHandler as Et, usePackageDiscoveryService as F, isDocumentTypeSupported as Fi, setSentryRelease as Fn, useWarnOutdatedApp as Fr, addRouterBasenameEventHandler as Ft, useFeatures as G, useDefaultDrivesUrl as Gn, setIsDragAndDropEnabled as Gr, phGlobalConfigSetters as Gt, useDropTarget as H, usePHModal as Hi, useAnalyticsDatabaseName as Hn, phDocumentEditorConfigHooks as Hr, addVersionEventHandler as Ht, addGraphQLReactorClientEventHandler as I, addModalEventHandler as Ii, setStudioMode as In, addAllowedDocumentTypesEventHandler as Ir, addSentryDsnEventHandler as It, useAttachmentService as J, useEnabledEditors as Jn, useIsDragAndDropEnabled as Jr, setBasePath as Jt, addAttachmentServiceEventHandler as K, useDisabledEditors as Kn, setIsExternalControlsEnabled as Kr, setAllowList as Kt, setGraphQLReactorClient as L, closePHModal as Li, setSwitchboardUrl as Ln, addIsDragAndDropEnabledEventHandler as Lr, addSentryEnvEventHandler as Lt, useSyncList as M, setPreferredEditorOnNode as Mi, setRouterBasename as Mn, useSwitchboardUrl as Mr, addRenownNetworkIdEventHandler as Mt, addPackageDiscoveryServiceEventHandler as N, upgradeDocument as Ni, setSentryDsn as Nn, useVersion as Nr, addRenownUrlEventHandler as Nt, usePGlite as O, loadFile as Oi, setRenownChainId as On, useSentryDsn as Or, addLocalDrivesEnabledEventHandler as Ot, setPackageDiscoveryService as P, getUserPermissions as Pi, setSentryEnv as Pn, useVersionCheckInterval as Pr, addRequiresHardRefreshEventHandler as Pt, addCliVersionEventHandler as Q, useIsAddDriveEnabled as Qn, viewFilterInputFromViewFilter as Qr, setDrivesPreserveStrategy as Qt, useGraphQLReactorClient as R, setPHModal as Ri, setVersion as Rn, addIsExternalControlsEnabledEventHandler as Rr, addSentryReleaseEventHandler as Rt, addReactorClientEventHandler as S, addDocument as Si, setIsPublicDrivesEnabled as Sn, useRenownAdapters as Sr, addIsExternalProcessorsEnabledEventHandler as St, setReactorClientModule as T, copyNode$1 as Ti, setLocalDrivesEnabled as Tn, useRenownUrl as Tr, addIsPublicDrivesEnabledEventHandler as Tt, addFeaturesEventHandler as U, useBasePath as Un, phDocumentEditorConfigSetters as Ur, addWarnOutdatedAppEventHandler as Ut, useDropNode as V, showPHModal as Vi, useAllowList as Vn, phAppConfigSetters as Vr, addVersionCheckIntervalEventHandler as Vt, setFeatures as W, useCliVersion as Wn, setAllowedDocumentTypes as Wr, phGlobalConfigHooks as Wt, addAnalyticsDatabaseNameEventHandler as X, useGaTrackingId as Xn, GraphQLReactorClient as Xr, setDefaultDrivesUrl as Xt, addAllowListEventHandler as Y, useFileUploadOperationsChunkSize as Yn, useIsExternalControlsEnabled as Yr, setCliVersion as Yt, addBasePathEventHandler as Z, useIsAddCloudDrivesEnabled as Zn, isGraphQLReactorClient as Zr, setDisabledEditors as Zt, addRevisionHistoryVisibleEventHandler as _, buildPulledDocument as _i, setIsEditorReadModeEnabled as _n, useIsPublicDrivesEnabled as _r, addIsDocumentModelSelectionSettingsEnabledEventHandler as _t, addPHEventHandlers as a, StaticPackageManager as ai, setIsAddPublicDrivesEnabled as an, useIsDeleteCloudDrivesEnabled as ar, addIsAddCloudDrivesEnabledEventHandler as at, showRevisionHistory as b, hasRevisionConflict as bi, setIsExternalRelationalProcessorsEnabled as bn, useLocalDrivesEnabled as br, addIsEditorReadModeEnabledEventHandler as bt, addToastEventHandler as c, prepareSignedActions as ci, setIsAnalyticsExternalProcessorsEnabled as cn, useIsDiffAnalyticsEnabled as cr, addIsAddPublicDrivesEnabledEventHandler as ct, addSelectedTimelineRevisionEventHandler as d, MutateDocumentWithOperationsDocument as di, setIsDeleteLocalDrivesEnabled as dn, useIsEditorDebugModeEnabled as dr, addIsAnalyticsExternalProcessorsEnabledEventHandler as dt, startDocumentChangesSubscription as ei, setFileUploadOperationsChunkSize as en, useIsAddPublicDrivesEnabled as er, addDisabledEditorsEventHandler as et, setSelectedTimelineRevision as f, ReactorOperationFieldsFragmentDoc as fi, setIsDeletePublicDrivesEnabled as fn, useIsEditorReadModeEnabled as fr, addIsCloudDrivesEnabledEventHandler as ft, useSelectedTimelineItem as g, ConflictError as gi, setIsEditorDebugModeEnabled as gn, useIsLocalDrivesEnabled as gr, addIsDiffAnalyticsEnabledEventHandler as gt, setSelectedTimelineItem as h, revisionMapFromRevisionsList as hi, setIsDriveAnalyticsEnabled as hn, useIsExternalRelationalProcessorsEnabled as hr, addIsDeletePublicDrivesEnabledEventHandler as ht, useSwitchboardClient as i, subgraphUrlFromGraphqlUrl as ii, setIsAddLocalDrivesEnabled as in, useIsCloudDrivesEnabled as ir, addGaTrackingIdEventHandler as it, useSync as j, renameNode as ji, setRequiresHardRefresh as jn, useStudioMode as jr, addRenownChainIdEventHandler as jt, useReactorClient as k, moveNode$1 as ki, setRenownNetworkId as kn, useSentryEnv as kr, addLogLevelEventHandler as kt, setPHToast as l, signStampedAction as li, setIsCloudDrivesEnabled as ln, useIsDocumentModelSelectionSettingsEnabled as lr, addIsAnalyticsDatabaseWorkerEnabledEventHandler as lt, addSelectedTimelineItemEventHandler as m, phDocumentFromMutation as mi, setIsDocumentModelSelectionSettingsEnabled as mn, useIsExternalProcessorsEnabled as mr, addIsDeleteLocalDrivesEnabledEventHandler as mt, GraphQLReactorProvider as n, SubgraphSdkRegistry as ni, setIsAddCloudDrivesEnabled as nn, useIsAnalyticsEnabled as nr, addEnabledEditorsEventHandler as nt, callEventHandlerRegisterFunctions as o, packageFromDocumentModels as oi, setIsAnalyticsDatabaseWorkerEnabled as on, useIsDeleteLocalDrivesEnabled as or, addIsAddDriveEnabledEventHandler as ot, useSelectedTimelineRevision as p, phDocumentFromGetDocument as pi, setIsDiffAnalyticsEnabled as pn, useIsExternalPackagesEnabled as pr, addIsDeleteCloudDrivesEnabledEventHandler as pt, setAttachmentService as q, useDrivesPreserveStrategy as qn, useAllowedDocumentTypes as qr, setAnalyticsDatabaseName as qt, ensurePHEventHandlers as r, describeGraphQLDocument as ri, setIsAddDriveEnabled as rn, useIsAnalyticsExternalProcessorsEnabled as rr, addFileUploadOperationsChunkSizeEventHandler as rt, commonGlobalEventHandlerFunctions as s, resolveDocumentModelModule as si, setIsAnalyticsEnabled as sn, useIsDeletePublicDrivesEnabled as sr, addIsAddLocalDrivesEnabledEventHandler as st, useDocumentOperations as t, subscriptionsUrlFromGraphqlUrl as ti, setGaTrackingId as tn, useIsAnalyticsDatabaseWorkerEnabled as tr, addDrivesPreserveStrategyEventHandler as tt, usePHToast as u, stampAction as ui, setIsDeleteCloudDrivesEnabled as un, useIsDriveAnalyticsEnabled as ur, addIsAnalyticsEnabledEventHandler as ut, hideRevisionHistory as v, convertRemoteOperations as vi, setIsExternalPackagesEnabled as vn, useIsRelationalProcessorsEnabled as vr, addIsDriveAnalyticsEnabledEventHandler as vt, setReactorClient as w, addFolder$1 as wi, setIsSentryTracingEnabled as wn, useRenownNetworkId as wr, addIsLocalDrivesEnabledEventHandler as wt, useRevisionHistoryVisible as x, screamingSnakeToCamel as xi, setIsLocalDrivesEnabled as xn, useLogLevel as xr, addIsExternalPackagesEnabledEventHandler as xt, setRevisionHistoryVisible as y, extractRevisionMap as yi, setIsExternalProcessorsEnabled as yn, useIsSentryTracingEnabled as yr, addIsEditorDebugModeEnabledEventHandler as yt, addDraggingNodeEventHandler as z, showCreateDocumentModal as zi, setVersionCheckInterval as zn, isExternalControlsEnabledEventFunctions as zr, addStudioModeEventHandler as zt };
|
|
2538
|
+
|
|
2539
|
+
//# sourceMappingURL=document-operations-BO-SlTDw.js.map
|