@fias/arche-sdk 2.3.0 → 2.11.0
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/dist/bridge.d.ts +76 -1
- package/dist/bridge.d.ts.map +1 -1
- package/dist/bridge.js +219 -6
- package/dist/bridge.js.map +1 -1
- package/dist/bridge.test.js +155 -6
- package/dist/bridge.test.js.map +1 -1
- package/dist/coverage-fills.test.js +26 -32
- package/dist/coverage-fills.test.js.map +1 -1
- package/dist/entity-ops.d.ts +55 -0
- package/dist/entity-ops.d.ts.map +1 -0
- package/dist/entity-ops.js +48 -0
- package/dist/entity-ops.js.map +1 -0
- package/dist/fias.d.ts +11 -1
- package/dist/fias.d.ts.map +1 -1
- package/dist/fias.js +39 -12
- package/dist/fias.js.map +1 -1
- package/dist/generated/permissions.d.ts +7 -1
- package/dist/generated/permissions.d.ts.map +1 -1
- package/dist/generated/permissions.js +35 -2
- package/dist/generated/permissions.js.map +1 -1
- package/dist/generated/surfaces.d.ts +424 -0
- package/dist/generated/surfaces.d.ts.map +1 -1
- package/dist/generated/surfaces.js +97 -1
- package/dist/generated/surfaces.js.map +1 -1
- package/dist/hooks.d.ts +228 -13
- package/dist/hooks.d.ts.map +1 -1
- package/dist/hooks.js +527 -104
- package/dist/hooks.js.map +1 -1
- package/dist/hooks.test.js +314 -247
- package/dist/hooks.test.js.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -2
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +982 -115
- package/dist/protocol.d.ts +7 -0
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +7 -0
- package/dist/protocol.js.map +1 -1
- package/dist/provider.d.ts +50 -1
- package/dist/provider.d.ts.map +1 -1
- package/dist/provider.js +187 -2
- package/dist/provider.js.map +1 -1
- package/dist/provider.render.test.d.ts +17 -0
- package/dist/provider.render.test.d.ts.map +1 -0
- package/dist/provider.render.test.js +191 -0
- package/dist/provider.render.test.js.map +1 -0
- package/dist/provider.test.js +64 -8
- package/dist/provider.test.js.map +1 -1
- package/dist/types.d.ts +373 -27
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -1
- package/templates/default/AGENTS.md +321 -34
- package/templates/default/CLAUDE.md +321 -34
package/dist/hooks.js
CHANGED
|
@@ -1,26 +1,33 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.GRAMMAR_CHECK_ENTITY_ID = exports.NLLB_LANGUAGES = exports.NLLB_TRANSLATION_ENTITY_ID = void 0;
|
|
3
4
|
exports.useFiasUser = useFiasUser;
|
|
4
5
|
exports.useFiasTheme = useFiasTheme;
|
|
6
|
+
exports.useFiasVendoredAssets = useFiasVendoredAssets;
|
|
5
7
|
exports.useFiasStorage = useFiasStorage;
|
|
6
8
|
exports.useEntityInvocation = useEntityInvocation;
|
|
7
9
|
exports.useImageGeneration = useImageGeneration;
|
|
8
10
|
exports.useImageEntities = useImageEntities;
|
|
9
|
-
exports.
|
|
11
|
+
exports.useClientEntity = useClientEntity;
|
|
10
12
|
exports.useAudioGeneration = useAudioGeneration;
|
|
11
13
|
exports.useSurface = useSurface;
|
|
12
14
|
exports.useFiasNavigation = useFiasNavigation;
|
|
13
15
|
exports.usePersistentState = usePersistentState;
|
|
14
16
|
exports.useStepNavigation = useStepNavigation;
|
|
15
17
|
exports.useFiasDataStore = useFiasDataStore;
|
|
18
|
+
exports.useFiasWorkspaces = useFiasWorkspaces;
|
|
16
19
|
exports.useFiasStore = useFiasStore;
|
|
17
20
|
exports.fetchVaultDocumentDownloadUrl = fetchVaultDocumentDownloadUrl;
|
|
18
21
|
exports.useVaultDocuments = useVaultDocuments;
|
|
19
22
|
exports.useArcheAssets = useArcheAssets;
|
|
23
|
+
exports.useFiasPreviewState = useFiasPreviewState;
|
|
24
|
+
exports.useDataSubscription = useDataSubscription;
|
|
25
|
+
exports.useVaultUserDocuments = useVaultUserDocuments;
|
|
20
26
|
const react_1 = require("react");
|
|
21
27
|
const react_dom_1 = require("react-dom");
|
|
22
28
|
const context_1 = require("./context");
|
|
23
29
|
const bridge_1 = require("./bridge");
|
|
30
|
+
const entity_ops_1 = require("./entity-ops");
|
|
24
31
|
/**
|
|
25
32
|
* Internal helper to get bridge from context, throws if not wrapped in FiasProvider.
|
|
26
33
|
*/
|
|
@@ -58,7 +65,7 @@ function createDebouncedWriter(bridge) {
|
|
|
58
65
|
if (timerId !== null)
|
|
59
66
|
clearTimeout(timerId);
|
|
60
67
|
pending = null;
|
|
61
|
-
bridge.
|
|
68
|
+
(0, entity_ops_1.invokeEntityOp)(bridge, entity_ops_1.PLUGIN_STORAGE_ENTITY_ID, 'write', { path, content }).catch(() => { });
|
|
62
69
|
}
|
|
63
70
|
function schedule(path, content) {
|
|
64
71
|
const now = Date.now();
|
|
@@ -84,21 +91,34 @@ function createDebouncedWriter(bridge) {
|
|
|
84
91
|
* Access the current user's profile information.
|
|
85
92
|
* Requires the `user:profile:read` permission in fias-plugin.json.
|
|
86
93
|
*
|
|
94
|
+
* If the `get_user` bridge request fails — most commonly because the plugin
|
|
95
|
+
* called this hook without declaring `user:profile:read` — the error is
|
|
96
|
+
* thrown during render so it surfaces to the nearest error boundary
|
|
97
|
+
* (`FiasProvider` installs one). Previously a failed/denied request left the
|
|
98
|
+
* hook permanently returning `null`, indistinguishable from "still loading",
|
|
99
|
+
* so a UI that gated on `if (!user)` hung on an infinite spinner forever.
|
|
100
|
+
*
|
|
87
101
|
* @returns The current user's profile or null while loading.
|
|
88
102
|
*/
|
|
89
103
|
function useFiasUser() {
|
|
90
104
|
const bridge = useBridge();
|
|
91
105
|
const [user, setUser] = (0, react_1.useState)(null);
|
|
106
|
+
const [error, setError] = (0, react_1.useState)(null);
|
|
92
107
|
(0, react_1.useEffect)(() => {
|
|
93
108
|
let mounted = true;
|
|
94
109
|
bridge.request('get_user', {}).then((data) => {
|
|
95
110
|
if (mounted)
|
|
96
111
|
setUser(data);
|
|
112
|
+
}, (err) => {
|
|
113
|
+
if (mounted)
|
|
114
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
97
115
|
});
|
|
98
116
|
return () => {
|
|
99
117
|
mounted = false;
|
|
100
118
|
};
|
|
101
119
|
}, [bridge]);
|
|
120
|
+
if (error)
|
|
121
|
+
throw error;
|
|
102
122
|
return user;
|
|
103
123
|
}
|
|
104
124
|
/**
|
|
@@ -116,6 +136,25 @@ function useFiasTheme() {
|
|
|
116
136
|
}, [bridge]);
|
|
117
137
|
return theme;
|
|
118
138
|
}
|
|
139
|
+
/**
|
|
140
|
+
* Self-hosted asset URLs for a platform-vendored library, for plugins that
|
|
141
|
+
* declare the `sandbox:vendored-libraries` permission. Returns a file-key → URL
|
|
142
|
+
* map (for `pdfjs-dist`: `{ mainModule, worker, cMapUrl, standardFontDataUrl }`),
|
|
143
|
+
* or `null` when the host's CDN is unconfigured or the library isn't vendored
|
|
144
|
+
* (the plugin should then degrade gracefully).
|
|
145
|
+
*
|
|
146
|
+
* Delivered in the init payload (no gated request); values are static for the
|
|
147
|
+
* session. The import specifier is already pinned to its main module via the
|
|
148
|
+
* host importmap, so a plugin typically only needs `worker` (pass to the
|
|
149
|
+
* pdf-core loader's `workerModuleUrl`) and the CMap/font directories.
|
|
150
|
+
*
|
|
151
|
+
* @param importSpecifier The vendored library's bare module name (e.g. `'pdfjs-dist'`).
|
|
152
|
+
* @returns The library's asset URLs, or null if unavailable.
|
|
153
|
+
*/
|
|
154
|
+
function useFiasVendoredAssets(importSpecifier) {
|
|
155
|
+
const bridge = useBridge();
|
|
156
|
+
return bridge.getVendoredAssets(importSpecifier);
|
|
157
|
+
}
|
|
119
158
|
/**
|
|
120
159
|
* Access the plugin's sandboxed storage.
|
|
121
160
|
* Requires the `storage:sandbox` permission in fias-plugin.json.
|
|
@@ -125,18 +164,23 @@ function useFiasTheme() {
|
|
|
125
164
|
function useFiasStorage() {
|
|
126
165
|
const bridge = useBridge();
|
|
127
166
|
const readFile = (0, react_1.useCallback)(async (path) => {
|
|
128
|
-
const res = await bridge.
|
|
167
|
+
const res = await (0, entity_ops_1.invokeEntityOp)(bridge, entity_ops_1.PLUGIN_STORAGE_ENTITY_ID, 'read', { path });
|
|
129
168
|
return res?.content ?? null;
|
|
130
169
|
}, [bridge]);
|
|
131
170
|
const writeFile = (0, react_1.useCallback)(async (path, content) => {
|
|
132
|
-
await bridge.
|
|
171
|
+
await (0, entity_ops_1.invokeEntityOp)(bridge, entity_ops_1.PLUGIN_STORAGE_ENTITY_ID, 'write', {
|
|
172
|
+
path,
|
|
173
|
+
content,
|
|
174
|
+
});
|
|
133
175
|
}, [bridge]);
|
|
134
176
|
const listFiles = (0, react_1.useCallback)(async (prefix) => {
|
|
135
|
-
const res = await bridge.
|
|
177
|
+
const res = await (0, entity_ops_1.invokeEntityOp)(bridge, entity_ops_1.PLUGIN_STORAGE_ENTITY_ID, 'list', { prefix });
|
|
136
178
|
return res?.files ?? [];
|
|
137
179
|
}, [bridge]);
|
|
138
180
|
const deleteFile = (0, react_1.useCallback)(async (path) => {
|
|
139
|
-
await bridge.
|
|
181
|
+
await (0, entity_ops_1.invokeEntityOp)(bridge, entity_ops_1.PLUGIN_STORAGE_ENTITY_ID, 'delete', {
|
|
182
|
+
path,
|
|
183
|
+
});
|
|
140
184
|
}, [bridge]);
|
|
141
185
|
return { readFile, writeFile, listFiles, deleteFile };
|
|
142
186
|
}
|
|
@@ -276,48 +320,63 @@ function useImageEntities(filter) {
|
|
|
276
320
|
}, [fetchOnce]);
|
|
277
321
|
return { entities, isLoading, error, refetch: fetchOnce };
|
|
278
322
|
}
|
|
279
|
-
/** Mirrors the host-side cap in the imgly executor. Pre-checked here so a
|
|
280
|
-
* 100 MB blob isn't structured-cloned across postMessage just to be
|
|
281
|
-
* rejected. */
|
|
282
|
-
const BG_REMOVAL_MAX_BYTES = 25 * 1024 * 1024;
|
|
283
323
|
/**
|
|
284
|
-
*
|
|
285
|
-
*
|
|
324
|
+
* `useBackgroundRemoval()` was REMOVED. It targeted the on-device imgly
|
|
325
|
+
* background-removal entity, which was retired platform-wide for
|
|
326
|
+
* licensing reasons (AGPL-3.0), along with its
|
|
327
|
+
* `entities:image_remove_background` permission.
|
|
286
328
|
*
|
|
287
|
-
*
|
|
288
|
-
* `
|
|
289
|
-
*
|
|
290
|
-
*
|
|
329
|
+
* There is no client-side replacement. Server-side background removal is
|
|
330
|
+
* available to plugins through the paid `image.smart-remove-background`
|
|
331
|
+
* invocation surface (Stability / Recraft) under the
|
|
332
|
+
* `entities:image_edit` permission.
|
|
291
333
|
*/
|
|
292
|
-
const IMGLY_BACKGROUND_REMOVAL_ENTITY_ID = 'ent_intg_imgly_bgrm_000000000000';
|
|
293
334
|
/**
|
|
294
|
-
*
|
|
295
|
-
*
|
|
296
|
-
*
|
|
297
|
-
|
|
335
|
+
* Default timeout for a client-side entity invocation. Client-side entities
|
|
336
|
+
* (WASM models, dictionary engines) can take many seconds to warm up and
|
|
337
|
+
* run, so we use the same 2-minute ceiling as the image/audio surfaces.
|
|
338
|
+
*/
|
|
339
|
+
const CLIENT_ENTITY_INVOKE_TIMEOUT_MS = 120000;
|
|
340
|
+
/**
|
|
341
|
+
* Invoke a client-side ("on-device") entity through the bridge's generic
|
|
342
|
+
* `client_entity_invoke` op. Client-side entities execute entirely in the
|
|
343
|
+
* browser (e.g. the nspell + retext grammar checker, the NLLB-200
|
|
344
|
+
* translator) — input never leaves the device and there is no per-call
|
|
345
|
+
* credit cost.
|
|
346
|
+
*
|
|
347
|
+
* This is the client-side counterpart to {@link useEntityInvocation}, which
|
|
348
|
+
* invokes server-side, credit-billed AI entities. Target an entity by its ID
|
|
349
|
+
* and supply the input shape that entity documents; the typed result flows
|
|
350
|
+
* back through the returned promise:
|
|
298
351
|
*
|
|
299
|
-
*
|
|
300
|
-
*
|
|
301
|
-
*
|
|
302
|
-
*
|
|
352
|
+
* ```tsx
|
|
353
|
+
* const { invoke, isLoading, error } = useClientEntity();
|
|
354
|
+
* const { issues } = await invoke<GrammarCheckInput, GrammarCheckResult>(
|
|
355
|
+
* GRAMMAR_CHECK_ENTITY_ID,
|
|
356
|
+
* { text },
|
|
357
|
+
* );
|
|
358
|
+
* ```
|
|
359
|
+
*
|
|
360
|
+
* Requires the `entities:client_invoke` permission in `fias-plugin.json`.
|
|
361
|
+
* Per-entity rate limits (each entity's `pluginInvocable.rateLimitPerMinute`)
|
|
362
|
+
* are enforced by the host.
|
|
363
|
+
*
|
|
364
|
+
* Input limits are enforced **host-side by each entity's executor**, not by
|
|
365
|
+
* this hook — being generic, it cannot know a given entity's caps (e.g. the
|
|
366
|
+
* grammar entity's 100,000-character limit). Oversized/invalid input is
|
|
367
|
+
* rejected after it crosses the bridge, surfacing as a rejected promise /
|
|
368
|
+
* `error`. If you want to fail fast before the round-trip, pre-validate in
|
|
369
|
+
* your own code against the limits the entity documents.
|
|
303
370
|
*/
|
|
304
|
-
function
|
|
371
|
+
function useClientEntity() {
|
|
305
372
|
const bridge = useBridge();
|
|
306
373
|
const [isLoading, setIsLoading] = (0, react_1.useState)(false);
|
|
307
|
-
const [result, setResult] = (0, react_1.useState)(null);
|
|
308
374
|
const [error, setError] = (0, react_1.useState)(null);
|
|
309
|
-
const
|
|
310
|
-
if (image.size > BG_REMOVAL_MAX_BYTES) {
|
|
311
|
-
const e = new Error('background-removal: image exceeds 25 MiB limit');
|
|
312
|
-
setError(e);
|
|
313
|
-
throw e;
|
|
314
|
-
}
|
|
375
|
+
const invoke = (0, react_1.useCallback)(async (entityId, input) => {
|
|
315
376
|
setIsLoading(true);
|
|
316
377
|
setError(null);
|
|
317
378
|
try {
|
|
318
|
-
|
|
319
|
-
setResult(res.image);
|
|
320
|
-
return res.image;
|
|
379
|
+
return await bridge.request('client_entity_invoke', { entityId, input }, CLIENT_ENTITY_INVOKE_TIMEOUT_MS);
|
|
321
380
|
}
|
|
322
381
|
catch (err) {
|
|
323
382
|
const e = err instanceof Error ? err : new Error(String(err));
|
|
@@ -328,8 +387,76 @@ function useBackgroundRemoval() {
|
|
|
328
387
|
setIsLoading(false);
|
|
329
388
|
}
|
|
330
389
|
}, [bridge]);
|
|
331
|
-
return {
|
|
390
|
+
return { invoke, isLoading, error };
|
|
332
391
|
}
|
|
392
|
+
/**
|
|
393
|
+
* Entity ID of the NLLB-200 on-device translation entity. Exported so plugins
|
|
394
|
+
* can target it via the generic {@link useClientEntity} hook, with
|
|
395
|
+
* `TranslateParams` / `TranslationResult` as the I/O types.
|
|
396
|
+
*
|
|
397
|
+
* SYNC NOTE: must match `NLLB_TRANSLATION_ENTITY_ID` in
|
|
398
|
+
* `packages/entities/src/integrations/nllb-translation/config.ts`. The
|
|
399
|
+
* architecture test `sdk-entity-id-constants-match.test.ts` pins this —
|
|
400
|
+
* drift fails CI.
|
|
401
|
+
*/
|
|
402
|
+
exports.NLLB_TRANSLATION_ENTITY_ID = 'ent_intg_nllb_xlate_000000000000';
|
|
403
|
+
/**
|
|
404
|
+
* Curated list of target languages for the NLLB translator, as
|
|
405
|
+
* label/FLORES-200-code pairs. Not exhaustive (NLLB supports 200 languages) —
|
|
406
|
+
* a convenient default set for building a picker. Sorted by label.
|
|
407
|
+
*/
|
|
408
|
+
exports.NLLB_LANGUAGES = [
|
|
409
|
+
{ label: 'Arabic', code: 'arb_Arab' },
|
|
410
|
+
{ label: 'Bengali', code: 'ben_Beng' },
|
|
411
|
+
{ label: 'Bulgarian', code: 'bul_Cyrl' },
|
|
412
|
+
{ label: 'Catalan', code: 'cat_Latn' },
|
|
413
|
+
{ label: 'Chinese (Simplified)', code: 'zho_Hans' },
|
|
414
|
+
{ label: 'Chinese (Traditional)', code: 'zho_Hant' },
|
|
415
|
+
{ label: 'Croatian', code: 'hrv_Latn' },
|
|
416
|
+
{ label: 'Czech', code: 'ces_Latn' },
|
|
417
|
+
{ label: 'Danish', code: 'dan_Latn' },
|
|
418
|
+
{ label: 'Dutch', code: 'nld_Latn' },
|
|
419
|
+
{ label: 'English', code: 'eng_Latn' },
|
|
420
|
+
{ label: 'Finnish', code: 'fin_Latn' },
|
|
421
|
+
{ label: 'French', code: 'fra_Latn' },
|
|
422
|
+
{ label: 'German', code: 'deu_Latn' },
|
|
423
|
+
{ label: 'Greek', code: 'ell_Grek' },
|
|
424
|
+
{ label: 'Hebrew', code: 'heb_Hebr' },
|
|
425
|
+
{ label: 'Hindi', code: 'hin_Deva' },
|
|
426
|
+
{ label: 'Hungarian', code: 'hun_Latn' },
|
|
427
|
+
{ label: 'Indonesian', code: 'ind_Latn' },
|
|
428
|
+
{ label: 'Italian', code: 'ita_Latn' },
|
|
429
|
+
{ label: 'Japanese', code: 'jpn_Jpan' },
|
|
430
|
+
{ label: 'Korean', code: 'kor_Hang' },
|
|
431
|
+
{ label: 'Norwegian', code: 'nob_Latn' },
|
|
432
|
+
{ label: 'Persian', code: 'pes_Arab' },
|
|
433
|
+
{ label: 'Polish', code: 'pol_Latn' },
|
|
434
|
+
{ label: 'Portuguese', code: 'por_Latn' },
|
|
435
|
+
{ label: 'Romanian', code: 'ron_Latn' },
|
|
436
|
+
{ label: 'Russian', code: 'rus_Cyrl' },
|
|
437
|
+
{ label: 'Serbian', code: 'srp_Cyrl' },
|
|
438
|
+
{ label: 'Slovak', code: 'slk_Latn' },
|
|
439
|
+
{ label: 'Spanish', code: 'spa_Latn' },
|
|
440
|
+
{ label: 'Swedish', code: 'swe_Latn' },
|
|
441
|
+
{ label: 'Tamil', code: 'tam_Taml' },
|
|
442
|
+
{ label: 'Telugu', code: 'tel_Telu' },
|
|
443
|
+
{ label: 'Thai', code: 'tha_Thai' },
|
|
444
|
+
{ label: 'Turkish', code: 'tur_Latn' },
|
|
445
|
+
{ label: 'Ukrainian', code: 'ukr_Cyrl' },
|
|
446
|
+
{ label: 'Urdu', code: 'urd_Arab' },
|
|
447
|
+
{ label: 'Vietnamese', code: 'vie_Latn' },
|
|
448
|
+
];
|
|
449
|
+
/**
|
|
450
|
+
* Entity ID of the grammar_check client-side entity. Exported so plugins can
|
|
451
|
+
* target it via the generic {@link useClientEntity} hook. Literal here so the
|
|
452
|
+
* SDK stays bundle-clean (no `@fias/entities` runtime dep).
|
|
453
|
+
*
|
|
454
|
+
* SYNC NOTE: must match `GRAMMAR_CHECK_ENTITY_ID` in
|
|
455
|
+
* `packages/entities/src/integrations/grammar-check/config.ts`. The
|
|
456
|
+
* architecture test `sdk-entity-id-constants-match.test.ts` pins this —
|
|
457
|
+
* drift fails CI.
|
|
458
|
+
*/
|
|
459
|
+
exports.GRAMMAR_CHECK_ENTITY_ID = 'ent_intg_grammar_check_000000000';
|
|
333
460
|
/**
|
|
334
461
|
* Generate audio (music today; future speech/SFX sub-types via the same
|
|
335
462
|
* hook) via the bridge's `audio_generate` op. Returns an array of clips
|
|
@@ -414,9 +541,10 @@ function useSurface(surfaceKey, timeoutMs = 180000) {
|
|
|
414
541
|
return { invoke, isLoading, result, error };
|
|
415
542
|
}
|
|
416
543
|
/**
|
|
417
|
-
* Navigate within the plugin's route space
|
|
544
|
+
* Navigate within the plugin's route space (`navigateTo`), or open another
|
|
545
|
+
* arche's page (`openArche`, requires the `navigation:open_arche` permission).
|
|
418
546
|
*
|
|
419
|
-
* @returns Navigation API (navigateTo, currentPath).
|
|
547
|
+
* @returns Navigation API (navigateTo, openArche, currentPath).
|
|
420
548
|
*/
|
|
421
549
|
function useFiasNavigation() {
|
|
422
550
|
const bridge = useBridge();
|
|
@@ -441,7 +569,10 @@ function useFiasNavigation() {
|
|
|
441
569
|
const navigateTo = (0, react_1.useCallback)((path) => {
|
|
442
570
|
bridge.request('navigate', { path });
|
|
443
571
|
}, [bridge]);
|
|
444
|
-
|
|
572
|
+
const openArche = (0, react_1.useCallback)((archeId, opts) => {
|
|
573
|
+
bridge.request('open_arche', { archeId, newTab: opts?.newTab === true });
|
|
574
|
+
}, [bridge]);
|
|
575
|
+
return { navigateTo, openArche, currentPath };
|
|
445
576
|
}
|
|
446
577
|
/**
|
|
447
578
|
* Like useState, but auto-persists to bridge storage.
|
|
@@ -459,8 +590,7 @@ function usePersistentState(key, initialValue) {
|
|
|
459
590
|
// Load from storage on mount; flush any pending write on unmount/key change
|
|
460
591
|
// so rapid updates in a game loop never lose their last value.
|
|
461
592
|
(0, react_1.useEffect)(() => {
|
|
462
|
-
bridge
|
|
463
|
-
.request('storage_read', { path: `__state/${key}` })
|
|
593
|
+
(0, entity_ops_1.invokeEntityOp)(bridge, entity_ops_1.PLUGIN_STORAGE_ENTITY_ID, 'read', { path: `__state/${key}` })
|
|
464
594
|
.then((result) => {
|
|
465
595
|
if (result.exists && result.content !== null) {
|
|
466
596
|
try {
|
|
@@ -473,6 +603,13 @@ function usePersistentState(key, initialValue) {
|
|
|
473
603
|
}
|
|
474
604
|
}
|
|
475
605
|
initializedRef.current = true;
|
|
606
|
+
})
|
|
607
|
+
.catch(() => {
|
|
608
|
+
// A failed/denied storage_read (e.g. the plugin didn't declare
|
|
609
|
+
// `storage:sandbox`) must not become a silent unhandled rejection.
|
|
610
|
+
// Persistence is best-effort — fall back to the initial value and
|
|
611
|
+
// let subsequent writes fail on their own rather than hanging here.
|
|
612
|
+
initializedRef.current = true;
|
|
476
613
|
});
|
|
477
614
|
return () => {
|
|
478
615
|
writer.flush();
|
|
@@ -514,10 +651,7 @@ function useStepNavigation(initialStep, options) {
|
|
|
514
651
|
return;
|
|
515
652
|
}
|
|
516
653
|
let cancelled = false;
|
|
517
|
-
bridge
|
|
518
|
-
.request('storage_read', {
|
|
519
|
-
path: `__state/${persistKey}`,
|
|
520
|
-
})
|
|
654
|
+
(0, entity_ops_1.invokeEntityOp)(bridge, entity_ops_1.PLUGIN_STORAGE_ENTITY_ID, 'read', { path: `__state/${persistKey}` })
|
|
521
655
|
.then((result) => {
|
|
522
656
|
// Don't clobber a user-initiated setCurrentStep that landed before this resolved.
|
|
523
657
|
if (cancelled || initializedRef.current)
|
|
@@ -590,34 +724,81 @@ function useStepNavigation(initialStep, options) {
|
|
|
590
724
|
function useFiasDataStore() {
|
|
591
725
|
const bridge = useBridge();
|
|
592
726
|
const createCollection = (0, react_1.useCallback)(async (name, options) => {
|
|
593
|
-
const res = await
|
|
727
|
+
const res = await (0, entity_ops_1.invokeEntityOp)(bridge, entity_ops_1.PLUGIN_DATASTORE_ENTITY_ID, 'create_collection', {
|
|
728
|
+
name,
|
|
729
|
+
userScope: options?.userScope ?? 'user',
|
|
730
|
+
searchable: options?.searchable,
|
|
731
|
+
writeMinRole: options?.writeMinRole,
|
|
732
|
+
readMinRole: options?.readMinRole,
|
|
733
|
+
writePolicy: options?.writePolicy,
|
|
734
|
+
});
|
|
594
735
|
return res.collection;
|
|
595
736
|
}, [bridge]);
|
|
596
737
|
const listCollections = (0, react_1.useCallback)(async () => {
|
|
597
|
-
const res = await bridge.
|
|
738
|
+
const res = await (0, entity_ops_1.invokeEntityOp)(bridge, entity_ops_1.PLUGIN_DATASTORE_ENTITY_ID, 'list_collections');
|
|
598
739
|
return res.collections;
|
|
599
740
|
}, [bridge]);
|
|
600
741
|
const deleteCollection = (0, react_1.useCallback)(async (name) => {
|
|
601
|
-
await bridge.
|
|
742
|
+
await (0, entity_ops_1.invokeEntityOp)(bridge, entity_ops_1.PLUGIN_DATASTORE_ENTITY_ID, 'delete_collection', { name });
|
|
602
743
|
}, [bridge]);
|
|
603
|
-
const put = (0, react_1.useCallback)(async (collection, key, data) => {
|
|
604
|
-
await
|
|
744
|
+
const put = (0, react_1.useCallback)(async (collection, key, data, options) => {
|
|
745
|
+
await (0, entity_ops_1.invokeEntityOp)(bridge, entity_ops_1.PLUGIN_DATASTORE_ENTITY_ID, 'put', {
|
|
746
|
+
collection,
|
|
747
|
+
key,
|
|
748
|
+
data,
|
|
749
|
+
workspaceId: options?.workspaceId,
|
|
750
|
+
});
|
|
605
751
|
}, [bridge]);
|
|
606
|
-
const get = (0, react_1.useCallback)(async (collection, key) => {
|
|
607
|
-
const res = await bridge.
|
|
752
|
+
const get = (0, react_1.useCallback)(async (collection, key, options) => {
|
|
753
|
+
const res = await (0, entity_ops_1.invokeEntityOp)(bridge, entity_ops_1.PLUGIN_DATASTORE_ENTITY_ID, 'get', {
|
|
608
754
|
collection,
|
|
609
755
|
key,
|
|
756
|
+
workspaceId: options?.workspaceId,
|
|
610
757
|
});
|
|
611
758
|
return res.data;
|
|
612
759
|
}, [bridge]);
|
|
613
|
-
const
|
|
614
|
-
|
|
760
|
+
const getDocument = (0, react_1.useCallback)(async (collection, key, options) => {
|
|
761
|
+
const res = await (0, entity_ops_1.invokeEntityOp)(bridge, entity_ops_1.PLUGIN_DATASTORE_ENTITY_ID, 'get', {
|
|
762
|
+
collection,
|
|
763
|
+
key,
|
|
764
|
+
workspaceId: options?.workspaceId,
|
|
765
|
+
});
|
|
766
|
+
if (res.data === null || res.data === undefined)
|
|
767
|
+
return null;
|
|
768
|
+
return {
|
|
769
|
+
key: res.key ?? key,
|
|
770
|
+
data: res.data,
|
|
771
|
+
updatedAt: res.updatedAt ?? '',
|
|
772
|
+
...(res.authoredByCollaborator !== undefined
|
|
773
|
+
? { authoredByCollaborator: res.authoredByCollaborator }
|
|
774
|
+
: {}),
|
|
775
|
+
};
|
|
776
|
+
}, [bridge]);
|
|
777
|
+
const query = (0, react_1.useCallback)(async (collection, options, scope) => {
|
|
778
|
+
return (0, entity_ops_1.invokeEntityOp)(bridge, entity_ops_1.PLUGIN_DATASTORE_ENTITY_ID, 'query', {
|
|
615
779
|
collection,
|
|
616
780
|
...options,
|
|
781
|
+
workspaceId: scope?.workspaceId,
|
|
617
782
|
});
|
|
618
783
|
}, [bridge]);
|
|
619
|
-
const
|
|
620
|
-
await bridge.
|
|
784
|
+
const search = (0, react_1.useCallback)(async (collection, queryText, options, scope) => {
|
|
785
|
+
const res = await (0, entity_ops_1.invokeEntityOp)(bridge, entity_ops_1.PLUGIN_DATASTORE_ENTITY_ID, 'search', {
|
|
786
|
+
collection,
|
|
787
|
+
query: queryText,
|
|
788
|
+
...options,
|
|
789
|
+
workspaceId: scope?.workspaceId,
|
|
790
|
+
});
|
|
791
|
+
return res.matches;
|
|
792
|
+
}, [bridge]);
|
|
793
|
+
const deleteDoc = (0, react_1.useCallback)(async (collection, key, options) => {
|
|
794
|
+
await (0, entity_ops_1.invokeEntityOp)(bridge, entity_ops_1.PLUGIN_DATASTORE_ENTITY_ID, 'delete', {
|
|
795
|
+
collection,
|
|
796
|
+
key,
|
|
797
|
+
workspaceId: options?.workspaceId,
|
|
798
|
+
});
|
|
799
|
+
}, [bridge]);
|
|
800
|
+
const batch = (0, react_1.useCallback)(async (operations) => {
|
|
801
|
+
await (0, entity_ops_1.invokeEntityOp)(bridge, entity_ops_1.PLUGIN_DATASTORE_ENTITY_ID, 'batch', { operations });
|
|
621
802
|
}, [bridge]);
|
|
622
803
|
return (0, react_1.useMemo)(() => ({
|
|
623
804
|
createCollection,
|
|
@@ -625,9 +806,84 @@ function useFiasDataStore() {
|
|
|
625
806
|
deleteCollection,
|
|
626
807
|
put,
|
|
627
808
|
get,
|
|
809
|
+
getDocument,
|
|
628
810
|
query,
|
|
811
|
+
search,
|
|
629
812
|
delete: deleteDoc,
|
|
630
|
-
|
|
813
|
+
batch,
|
|
814
|
+
}), [
|
|
815
|
+
createCollection,
|
|
816
|
+
listCollections,
|
|
817
|
+
deleteCollection,
|
|
818
|
+
put,
|
|
819
|
+
get,
|
|
820
|
+
getDocument,
|
|
821
|
+
query,
|
|
822
|
+
search,
|
|
823
|
+
deleteDoc,
|
|
824
|
+
batch,
|
|
825
|
+
]);
|
|
826
|
+
}
|
|
827
|
+
/**
|
|
828
|
+
* Access workspace tenancy — team/org workspaces with role-gated membership
|
|
829
|
+
* (owner/admin/member/viewer) over `workspace`-scoped Data Store collections
|
|
830
|
+
* (ADR-010). The creating user becomes the founding owner; a workspace always
|
|
831
|
+
* keeps at least one owner.
|
|
832
|
+
*
|
|
833
|
+
* Requires the `data:workspace` permission in fias-plugin.json. To read/write a
|
|
834
|
+
* workspace's documents, use `useFiasDataStore()` with `{ workspaceId }` on a
|
|
835
|
+
* collection created with `userScope: 'workspace'`.
|
|
836
|
+
*
|
|
837
|
+
* @example
|
|
838
|
+
* const workspaces = useFiasWorkspaces();
|
|
839
|
+
* const ws = await workspaces.create('Acme Corp');
|
|
840
|
+
* await workspaces.addMember(ws.workspaceId, otherUserId, 'member');
|
|
841
|
+
* // then, with useFiasDataStore():
|
|
842
|
+
* await dataStore.put('ledger', 'tx-1', { amount: 100 }, { workspaceId: ws.workspaceId });
|
|
843
|
+
*
|
|
844
|
+
* @returns Workspace API with lifecycle + membership methods.
|
|
845
|
+
*/
|
|
846
|
+
function useFiasWorkspaces() {
|
|
847
|
+
const bridge = useBridge();
|
|
848
|
+
const create = (0, react_1.useCallback)(async (displayName) => {
|
|
849
|
+
const res = await (0, entity_ops_1.invokeEntityOp)(bridge, entity_ops_1.PLUGIN_WORKSPACES_ENTITY_ID, 'create', { displayName });
|
|
850
|
+
return res.workspace;
|
|
851
|
+
}, [bridge]);
|
|
852
|
+
const list = (0, react_1.useCallback)(async () => {
|
|
853
|
+
const res = await (0, entity_ops_1.invokeEntityOp)(bridge, entity_ops_1.PLUGIN_WORKSPACES_ENTITY_ID, 'list');
|
|
854
|
+
return res.workspaces;
|
|
855
|
+
}, [bridge]);
|
|
856
|
+
const get = (0, react_1.useCallback)(async (workspaceId) => {
|
|
857
|
+
const res = await (0, entity_ops_1.invokeEntityOp)(bridge, entity_ops_1.PLUGIN_WORKSPACES_ENTITY_ID, 'get', { workspaceId });
|
|
858
|
+
return res.workspace;
|
|
859
|
+
}, [bridge]);
|
|
860
|
+
const archive = (0, react_1.useCallback)(async (workspaceId) => {
|
|
861
|
+
await (0, entity_ops_1.invokeEntityOp)(bridge, entity_ops_1.PLUGIN_WORKSPACES_ENTITY_ID, 'archive', { workspaceId });
|
|
862
|
+
}, [bridge]);
|
|
863
|
+
const listMembers = (0, react_1.useCallback)(async (workspaceId) => {
|
|
864
|
+
const res = await (0, entity_ops_1.invokeEntityOp)(bridge, entity_ops_1.PLUGIN_WORKSPACES_ENTITY_ID, 'list_members', { workspaceId });
|
|
865
|
+
return res.members;
|
|
866
|
+
}, [bridge]);
|
|
867
|
+
const addMember = (0, react_1.useCallback)(async (workspaceId, username, role, expiresAt) => {
|
|
868
|
+
const res = await (0, entity_ops_1.invokeEntityOp)(bridge, entity_ops_1.PLUGIN_WORKSPACES_ENTITY_ID, 'add_member', {
|
|
869
|
+
workspaceId,
|
|
870
|
+
username,
|
|
871
|
+
role,
|
|
872
|
+
expiresAt,
|
|
873
|
+
});
|
|
874
|
+
return res.member;
|
|
875
|
+
}, [bridge]);
|
|
876
|
+
const updateMember = (0, react_1.useCallback)(async (workspaceId, username, role) => {
|
|
877
|
+
const res = await (0, entity_ops_1.invokeEntityOp)(bridge, entity_ops_1.PLUGIN_WORKSPACES_ENTITY_ID, 'update_member', { workspaceId, username, role });
|
|
878
|
+
return res.member;
|
|
879
|
+
}, [bridge]);
|
|
880
|
+
const removeMember = (0, react_1.useCallback)(async (workspaceId, username) => {
|
|
881
|
+
await (0, entity_ops_1.invokeEntityOp)(bridge, entity_ops_1.PLUGIN_WORKSPACES_ENTITY_ID, 'remove_member', {
|
|
882
|
+
workspaceId,
|
|
883
|
+
username,
|
|
884
|
+
});
|
|
885
|
+
}, [bridge]);
|
|
886
|
+
return (0, react_1.useMemo)(() => ({ create, list, get, archive, listMembers, addMember, updateMember, removeMember }), [create, list, get, archive, listMembers, addMember, updateMember, removeMember]);
|
|
631
887
|
}
|
|
632
888
|
/**
|
|
633
889
|
* Access the in-arche purchase (IAP) store.
|
|
@@ -647,8 +903,8 @@ function useFiasStore() {
|
|
|
647
903
|
(0, react_1.useEffect)(() => {
|
|
648
904
|
let mounted = true;
|
|
649
905
|
Promise.all([
|
|
650
|
-
|
|
651
|
-
|
|
906
|
+
(0, entity_ops_1.invokeStoreOp)(bridge, 'get_products'),
|
|
907
|
+
(0, entity_ops_1.invokeStoreOp)(bridge, 'get_entitlements'),
|
|
652
908
|
])
|
|
653
909
|
.then(([prods, ents]) => {
|
|
654
910
|
if (mounted) {
|
|
@@ -666,7 +922,7 @@ function useFiasStore() {
|
|
|
666
922
|
};
|
|
667
923
|
}, [bridge]);
|
|
668
924
|
const purchase = (0, react_1.useCallback)(async (productIdentifier, options) => {
|
|
669
|
-
const result = await
|
|
925
|
+
const result = await (0, entity_ops_1.invokeStoreOp)(bridge, 'purchase', {
|
|
670
926
|
productIdentifier,
|
|
671
927
|
...options,
|
|
672
928
|
});
|
|
@@ -688,11 +944,11 @@ function useFiasStore() {
|
|
|
688
944
|
return entitlements.some((e) => e.productIdentifier === productIdentifier && e.isActive);
|
|
689
945
|
}, [entitlements]);
|
|
690
946
|
const getPurchaseHistory = (0, react_1.useCallback)(async () => {
|
|
691
|
-
const res = await
|
|
947
|
+
const res = await (0, entity_ops_1.invokeStoreOp)(bridge, 'get_purchase_history');
|
|
692
948
|
return res.purchases;
|
|
693
949
|
}, [bridge]);
|
|
694
950
|
const restorePurchases = (0, react_1.useCallback)(async () => {
|
|
695
|
-
const restored = await
|
|
951
|
+
const restored = await (0, entity_ops_1.invokeStoreOp)(bridge, 'restore');
|
|
696
952
|
setEntitlements(restored);
|
|
697
953
|
return restored;
|
|
698
954
|
}, [bridge]);
|
|
@@ -871,37 +1127,24 @@ function useVaultDocuments() {
|
|
|
871
1127
|
});
|
|
872
1128
|
}, [bridge]);
|
|
873
1129
|
const upload = (0, react_1.useCallback)(async (bytes, params) => {
|
|
874
|
-
//
|
|
875
|
-
//
|
|
876
|
-
//
|
|
877
|
-
//
|
|
1130
|
+
// The presigned PUT must run in the HOST, not here: the plugin iframe's
|
|
1131
|
+
// CSP is `connect-src 'none'`, which silently blocks an in-iframe
|
|
1132
|
+
// `fetch` PUT in published plugins. The host (parent window, no
|
|
1133
|
+
// connect-src restriction) orchestrates init → PUT → finalize via the
|
|
1134
|
+
// `vault_documents_upload` bridge op; the presigned URL is backend-issued
|
|
1135
|
+
// host-side, so the iframe can't redirect the PUT (no SSRF). The
|
|
1136
|
+
// lower-level `uploadInit`/`uploadFinalize` remain for advanced callers.
|
|
878
1137
|
const src = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
// (the Fetch spec lists it as forbidden — the browser computes
|
|
888
|
-
// it from `body.byteLength` automatically). Filter it out so the
|
|
889
|
-
// fetch doesn't either error in strict mode or silently send a
|
|
890
|
-
// different length than what we tried to pass. The signed value
|
|
891
|
-
// still matches because S3 sees the auto-set length, which the
|
|
892
|
-
// server matched against `sizeBytes` at init time.
|
|
893
|
-
const browserHeaders = Object.fromEntries(Object.entries(init.requiredHeaders).filter(([k]) => k.toLowerCase() !== 'content-length'));
|
|
894
|
-
const putResp = await fetch(init.uploadUrl, {
|
|
895
|
-
method: 'PUT',
|
|
896
|
-
body: u8,
|
|
897
|
-
headers: browserHeaders,
|
|
1138
|
+
return bridge.request('vault_documents_upload', {
|
|
1139
|
+
name: params.name,
|
|
1140
|
+
mimeType: params.mimeType,
|
|
1141
|
+
bytes: bytesToBase64(src),
|
|
1142
|
+
tags: params.tags,
|
|
1143
|
+
sensitivity: params.sensitivity,
|
|
1144
|
+
folderPath: params.folderPath,
|
|
1145
|
+
replacesDocumentId: params.replacesDocumentId,
|
|
898
1146
|
});
|
|
899
|
-
|
|
900
|
-
const text = await putResp.text().catch(() => '');
|
|
901
|
-
throw new Error(`Vault upload PUT failed (${putResp.status}): ${text || putResp.statusText}`);
|
|
902
|
-
}
|
|
903
|
-
return uploadFinalize({ documentId: init.documentId, checksumSha256: checksumHex });
|
|
904
|
-
}, [uploadInit, uploadFinalize]);
|
|
1147
|
+
}, [bridge]);
|
|
905
1148
|
return (0, react_1.useMemo)(() => ({
|
|
906
1149
|
list,
|
|
907
1150
|
read,
|
|
@@ -930,19 +1173,15 @@ function useVaultDocuments() {
|
|
|
930
1173
|
uploadFinalize,
|
|
931
1174
|
]);
|
|
932
1175
|
}
|
|
933
|
-
/**
|
|
934
|
-
*
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
function bytesToHex(bytes) {
|
|
941
|
-
let out = '';
|
|
942
|
-
for (let i = 0; i < bytes.length; i++) {
|
|
943
|
-
out += bytes[i].toString(16).padStart(2, '0');
|
|
1176
|
+
/** Base64-encode bytes for transport to the host's `vault_documents_upload`
|
|
1177
|
+
* op (the host does the presigned PUT — see PluginBridge.handleVaultUpload). */
|
|
1178
|
+
function bytesToBase64(bytes) {
|
|
1179
|
+
let binary = '';
|
|
1180
|
+
const chunk = 0x8000;
|
|
1181
|
+
for (let i = 0; i < bytes.length; i += chunk) {
|
|
1182
|
+
binary += String.fromCharCode(...bytes.subarray(i, i + chunk));
|
|
944
1183
|
}
|
|
945
|
-
return
|
|
1184
|
+
return btoa(binary);
|
|
946
1185
|
}
|
|
947
1186
|
/**
|
|
948
1187
|
* Access the contributor-curated asset library for this arche.
|
|
@@ -980,4 +1219,188 @@ function useArcheAssets() {
|
|
|
980
1219
|
}, [bridge]);
|
|
981
1220
|
return (0, react_1.useMemo)(() => ({ list, index, getUrl }), [list, index, getUrl]);
|
|
982
1221
|
}
|
|
1222
|
+
/**
|
|
1223
|
+
* Preserve transient UI state across a builder-preview rebuild.
|
|
1224
|
+
*
|
|
1225
|
+
* In the arche builder, editing code (or a background cross-device sync)
|
|
1226
|
+
* rebuilds the preview, which reloads the iframe and wipes the plugin's
|
|
1227
|
+
* in-memory state — losing, for example, a report the user just generated.
|
|
1228
|
+
* This hook lets you opt that state into host-side preservation: just before
|
|
1229
|
+
* the reload the host snapshots `getState()`, and after the reload it hands the
|
|
1230
|
+
* value back to `onRestore`.
|
|
1231
|
+
*
|
|
1232
|
+
* Inert outside the builder preview (the host never requests a capture), so
|
|
1233
|
+
* it's safe to leave in production plugin code.
|
|
1234
|
+
*
|
|
1235
|
+
* @param key Stable namespace for this slice of state (so multiple calls
|
|
1236
|
+
* don't collide). Use a literal like `'report'`.
|
|
1237
|
+
* @param getState Returns the current serializable state. Must be JSON-safe.
|
|
1238
|
+
* @param onRestore Receives the snapshot after a reload. Reads the latest
|
|
1239
|
+
* closure via a ref, so you don't need a stable identity.
|
|
1240
|
+
*
|
|
1241
|
+
* @example
|
|
1242
|
+
* const [report, setReport] = useState<Report | null>(null);
|
|
1243
|
+
* useFiasPreviewState('report', () => report, (saved) => setReport(saved as Report));
|
|
1244
|
+
*/
|
|
1245
|
+
function useFiasPreviewState(key, getState, onRestore) {
|
|
1246
|
+
const bridge = useBridge();
|
|
1247
|
+
const getStateRef = (0, react_1.useRef)(getState);
|
|
1248
|
+
getStateRef.current = getState;
|
|
1249
|
+
const onRestoreRef = (0, react_1.useRef)(onRestore);
|
|
1250
|
+
onRestoreRef.current = onRestore;
|
|
1251
|
+
(0, react_1.useEffect)(() => {
|
|
1252
|
+
return bridge.registerPreviewState(key, () => getStateRef.current(), (state) => onRestoreRef.current(state));
|
|
1253
|
+
}, [bridge, key]);
|
|
1254
|
+
}
|
|
1255
|
+
/**
|
|
1256
|
+
* Subscribes to realtime change events for a datastore collection
|
|
1257
|
+
* (ADR-012 Phase M4). Notify-then-refetch: the platform pushes a minimal
|
|
1258
|
+
* "this collection changed" invalidation and `onInvalidate` decides what to
|
|
1259
|
+
* refetch — typically re-running the `useFiasDataStore().query(...)` your
|
|
1260
|
+
* component renders from.
|
|
1261
|
+
*
|
|
1262
|
+
* Requires the `data:store` permission (same as reading the collection —
|
|
1263
|
+
* a live invalidation grants nothing polling couldn't retrieve).
|
|
1264
|
+
*
|
|
1265
|
+
* Semantics and guarantees:
|
|
1266
|
+
* - Best-effort, at-most-once delivery: events can be lost or duplicated;
|
|
1267
|
+
* the initial refetch fires AFTER the subscription is live, so a write
|
|
1268
|
+
* racing the subscribe is never missed.
|
|
1269
|
+
* - `onInvalidate` runs single-flight with one trailing call: a burst of
|
|
1270
|
+
* events during an in-flight refetch coalesces into exactly one more.
|
|
1271
|
+
* - The host renews authorization periodically; when it definitively ends
|
|
1272
|
+
* (`status: 'ended'`), `endedReason` says why — `collection_deleted` and
|
|
1273
|
+
* `authorization_revoked` are terminal, others may warrant resubscribing
|
|
1274
|
+
* (unmount/remount or toggle `enabled`).
|
|
1275
|
+
* - Scope-aware: shared collections notify on any user's writes; user-scoped
|
|
1276
|
+
* collections only on yours; workspace-scoped need `workspaceId` and an
|
|
1277
|
+
* active membership.
|
|
1278
|
+
*
|
|
1279
|
+
* @example
|
|
1280
|
+
* const dataStore = useFiasDataStore();
|
|
1281
|
+
* const [scores, setScores] = useState<DataStoreDocument[]>([]);
|
|
1282
|
+
* const refresh = useCallback(async () => {
|
|
1283
|
+
* const res = await dataStore.query('scores', { limit: 50 });
|
|
1284
|
+
* setScores(res.documents);
|
|
1285
|
+
* }, [dataStore]);
|
|
1286
|
+
* const { status } = useDataSubscription('scores', refresh);
|
|
1287
|
+
*/
|
|
1288
|
+
function useDataSubscription(collection, onInvalidate, options) {
|
|
1289
|
+
const bridge = useBridge();
|
|
1290
|
+
const [state, setState] = (0, react_1.useState)({ status: 'subscribing' });
|
|
1291
|
+
const onInvalidateRef = (0, react_1.useRef)(onInvalidate);
|
|
1292
|
+
onInvalidateRef.current = onInvalidate;
|
|
1293
|
+
const enabled = options?.enabled ?? true;
|
|
1294
|
+
const workspaceId = options?.workspaceId;
|
|
1295
|
+
(0, react_1.useEffect)(() => {
|
|
1296
|
+
if (!enabled) {
|
|
1297
|
+
setState({ status: 'ended', endedReason: 'unsubscribed' });
|
|
1298
|
+
return;
|
|
1299
|
+
}
|
|
1300
|
+
let cancelled = false;
|
|
1301
|
+
let subscriptionId = null;
|
|
1302
|
+
let unsubscribeEvent = null;
|
|
1303
|
+
let unsubscribeEnded = null;
|
|
1304
|
+
// Single-flight + exactly one trailing call: events arriving during an
|
|
1305
|
+
// in-flight refetch coalesce into one follow-up, never a parallel run.
|
|
1306
|
+
let refetching = false;
|
|
1307
|
+
let trailing = false;
|
|
1308
|
+
const runInvalidate = async () => {
|
|
1309
|
+
if (refetching) {
|
|
1310
|
+
trailing = true;
|
|
1311
|
+
return;
|
|
1312
|
+
}
|
|
1313
|
+
refetching = true;
|
|
1314
|
+
try {
|
|
1315
|
+
await onInvalidateRef.current();
|
|
1316
|
+
}
|
|
1317
|
+
catch {
|
|
1318
|
+
// The caller's refetch failed — their concern; the subscription
|
|
1319
|
+
// stays live and the next event tries again.
|
|
1320
|
+
}
|
|
1321
|
+
refetching = false;
|
|
1322
|
+
if (trailing && !cancelled) {
|
|
1323
|
+
trailing = false;
|
|
1324
|
+
void runInvalidate();
|
|
1325
|
+
}
|
|
1326
|
+
};
|
|
1327
|
+
setState({ status: 'subscribing' });
|
|
1328
|
+
void (async () => {
|
|
1329
|
+
try {
|
|
1330
|
+
const res = await bridge.request('entity_invoke', {
|
|
1331
|
+
entityId: entity_ops_1.PLUGIN_DATASTORE_ENTITY_ID,
|
|
1332
|
+
payload: { op: 'subscribe', collection, workspaceId },
|
|
1333
|
+
});
|
|
1334
|
+
const subscription = res?.output;
|
|
1335
|
+
if (!subscription ||
|
|
1336
|
+
subscription.kind !== 'subscription' ||
|
|
1337
|
+
typeof subscription.subscriptionId !== 'string') {
|
|
1338
|
+
if (!cancelled)
|
|
1339
|
+
setState({ status: 'ended', endedReason: 'transport_error' });
|
|
1340
|
+
return;
|
|
1341
|
+
}
|
|
1342
|
+
if (cancelled) {
|
|
1343
|
+
bridge.releaseSubscription(subscription.subscriptionId);
|
|
1344
|
+
return;
|
|
1345
|
+
}
|
|
1346
|
+
subscriptionId = subscription.subscriptionId;
|
|
1347
|
+
unsubscribeEvent = bridge.onSubscriptionEvent(subscriptionId, () => void runInvalidate());
|
|
1348
|
+
unsubscribeEnded = bridge.onSubscriptionEnded(subscriptionId, (reason) => {
|
|
1349
|
+
if (!cancelled)
|
|
1350
|
+
setState({ status: 'ended', endedReason: reason });
|
|
1351
|
+
});
|
|
1352
|
+
setState({ status: 'active' });
|
|
1353
|
+
// Subscribe-then-fetch ordering: refetch AFTER the subscription is
|
|
1354
|
+
// live so a write racing the handshake is reflected either in this
|
|
1355
|
+
// fetch or in the event it triggers — never silently missed.
|
|
1356
|
+
void runInvalidate();
|
|
1357
|
+
}
|
|
1358
|
+
catch {
|
|
1359
|
+
if (!cancelled)
|
|
1360
|
+
setState({ status: 'ended', endedReason: 'transport_error' });
|
|
1361
|
+
}
|
|
1362
|
+
})();
|
|
1363
|
+
return () => {
|
|
1364
|
+
cancelled = true;
|
|
1365
|
+
unsubscribeEvent?.();
|
|
1366
|
+
unsubscribeEnded?.();
|
|
1367
|
+
if (subscriptionId)
|
|
1368
|
+
bridge.releaseSubscription(subscriptionId);
|
|
1369
|
+
};
|
|
1370
|
+
}, [bridge, collection, workspaceId, enabled]);
|
|
1371
|
+
return state;
|
|
1372
|
+
}
|
|
1373
|
+
/**
|
|
1374
|
+
* Consented access to documents the USER already owns — the only SDK
|
|
1375
|
+
* surface that reaches data your plugin did not create. Requires the
|
|
1376
|
+
* `vault:user-documents:read` permission. Every access is user-consented
|
|
1377
|
+
* (per-document grants created in the host picker, revocable any time in
|
|
1378
|
+
* Vault → App Access) and audited by the platform. Proprietary-sensitivity
|
|
1379
|
+
* documents are never grantable. For documents your plugin CREATES, use
|
|
1380
|
+
* `useVaultDocuments` instead.
|
|
1381
|
+
*/
|
|
1382
|
+
function useVaultUserDocuments() {
|
|
1383
|
+
const bridge = useBridge();
|
|
1384
|
+
const pick = (0, react_1.useCallback)(async (options) => {
|
|
1385
|
+
return bridge.request('vault_documents_pick', {
|
|
1386
|
+
maxDocuments: options?.maxDocuments,
|
|
1387
|
+
});
|
|
1388
|
+
}, [bridge]);
|
|
1389
|
+
const list = (0, react_1.useCallback)(async (options) => {
|
|
1390
|
+
const res = await (0, entity_ops_1.invokeEntityOp)(bridge, entity_ops_1.VAULT_USER_DOCUMENTS_ENTITY_ID, 'list_consented_documents', { limit: options?.limit, offset: options?.offset });
|
|
1391
|
+
return res.documents;
|
|
1392
|
+
}, [bridge]);
|
|
1393
|
+
const get = (0, react_1.useCallback)(async (documentId, options) => {
|
|
1394
|
+
return (0, entity_ops_1.invokeEntityOp)(bridge, entity_ops_1.VAULT_USER_DOCUMENTS_ENTITY_ID, 'get_consented_document', { documentId, includeContent: options?.includeContent });
|
|
1395
|
+
}, [bridge]);
|
|
1396
|
+
const getDownloadUrl = (0, react_1.useCallback)(async (documentId) => {
|
|
1397
|
+
const res = await (0, entity_ops_1.invokeEntityOp)(bridge, entity_ops_1.VAULT_USER_DOCUMENTS_ENTITY_ID, 'get_consented_document_url', { documentId });
|
|
1398
|
+
return {
|
|
1399
|
+
url: res.url,
|
|
1400
|
+
expiresAt: new Date(res.expiresAt).getTime(),
|
|
1401
|
+
contentType: res.contentType,
|
|
1402
|
+
};
|
|
1403
|
+
}, [bridge]);
|
|
1404
|
+
return (0, react_1.useMemo)(() => ({ pick, list, get, getDownloadUrl }), [pick, list, get, getDownloadUrl]);
|
|
1405
|
+
}
|
|
983
1406
|
//# sourceMappingURL=hooks.js.map
|