@fias/arche-sdk 2.27.0 → 2.28.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/generated/permissions.d.ts +1 -1
- package/dist/generated/permissions.d.ts.map +1 -1
- package/dist/generated/permissions.js +3 -1
- package/dist/generated/permissions.js.map +1 -1
- package/dist/hooks.d.ts +113 -4
- package/dist/hooks.d.ts.map +1 -1
- package/dist/hooks.js +420 -4
- package/dist/hooks.js.map +1 -1
- package/dist/hooks.test.js +329 -0
- package/dist/hooks.test.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -2
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +333 -3
- package/dist/types.d.ts +252 -11
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/templates/default/AGENTS.md +142 -35
- package/templates/default/CLAUDE.md +142 -35
package/dist/hooks.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.FiasContentError = exports.PLUGIN_CONTENT_ENTITY_ID = exports.GRAMMAR_CHECK_ENTITY_ID = exports.NLLB_LANGUAGES = exports.NLLB_TRANSLATION_ENTITY_ID = void 0;
|
|
3
|
+
exports.FiasContentError = exports.FiasLocalStoreError = exports.PLUGIN_LOCAL_STORE_ENTITY_ID = exports.PLUGIN_CONTENT_ENTITY_ID = exports.ArcheAssetError = exports.ARCHE_ASSET_BYTES_ENTITY_ID = exports.GRAMMAR_CHECK_ENTITY_ID = exports.NLLB_LANGUAGES = exports.NLLB_TRANSLATION_ENTITY_ID = void 0;
|
|
4
4
|
exports.useFiasUser = useFiasUser;
|
|
5
5
|
exports.useFiasTheme = useFiasTheme;
|
|
6
6
|
exports.useFiasVendoredAssets = useFiasVendoredAssets;
|
|
@@ -21,6 +21,8 @@ exports.useFiasStore = useFiasStore;
|
|
|
21
21
|
exports.fetchVaultDocumentDownloadUrl = fetchVaultDocumentDownloadUrl;
|
|
22
22
|
exports.useVaultDocuments = useVaultDocuments;
|
|
23
23
|
exports.useArcheAssets = useArcheAssets;
|
|
24
|
+
exports.useArcheAssetBytes = useArcheAssetBytes;
|
|
25
|
+
exports.useLocalStore = useLocalStore;
|
|
24
26
|
exports.useFiasContent = useFiasContent;
|
|
25
27
|
exports.useFiasPreviewState = useFiasPreviewState;
|
|
26
28
|
exports.useDataSubscription = useDataSubscription;
|
|
@@ -1457,6 +1459,16 @@ function bytesToBase64(bytes) {
|
|
|
1457
1459
|
* state, and refresh `signedUrl` via `getUrl(assetId)` when it's about
|
|
1458
1460
|
* to expire.
|
|
1459
1461
|
*
|
|
1462
|
+
* The library holds images and short sound effects (MP3 / WAV). `list` and
|
|
1463
|
+
* `index` return images unless you pass `kinds` (e.g. `{ kinds: ['audio'] }`),
|
|
1464
|
+
* and with `kinds` the entries are typed `ArcheLibraryAsset` — narrow on
|
|
1465
|
+
* `contentType` before using an image's size. Keeping sounds here is
|
|
1466
|
+
* optional; music is not accepted. Sound effects that must play on cue need
|
|
1467
|
+
* decoded Web Audio buffers: load library sounds with
|
|
1468
|
+
* {@link useArcheAssetBytes}`().getMany` once per session, or bundle them in
|
|
1469
|
+
* the plugin's JS (base64) — not in `public/`, which only an `<audio>`
|
|
1470
|
+
* element can play.
|
|
1471
|
+
*
|
|
1460
1472
|
* Permission: `assets:read`.
|
|
1461
1473
|
*
|
|
1462
1474
|
* @example
|
|
@@ -1471,12 +1483,14 @@ function useArcheAssets() {
|
|
|
1471
1483
|
cursor: options?.cursor,
|
|
1472
1484
|
limit: options?.limit,
|
|
1473
1485
|
tags: options?.tags,
|
|
1486
|
+
kinds: options?.kinds,
|
|
1474
1487
|
});
|
|
1475
1488
|
}, [bridge]);
|
|
1476
1489
|
const index = (0, react_1.useCallback)(async (options) => {
|
|
1477
1490
|
return bridge.request('arche_assets_index', {
|
|
1478
1491
|
tags: options?.tags,
|
|
1479
1492
|
groupBy: options?.groupBy,
|
|
1493
|
+
kinds: options?.kinds,
|
|
1480
1494
|
});
|
|
1481
1495
|
}, [bridge]);
|
|
1482
1496
|
const getUrl = (0, react_1.useCallback)(async (assetId) => {
|
|
@@ -1484,6 +1498,269 @@ function useArcheAssets() {
|
|
|
1484
1498
|
}, [bridge]);
|
|
1485
1499
|
return (0, react_1.useMemo)(() => ({ list, index, getUrl }), [list, index, getUrl]);
|
|
1486
1500
|
}
|
|
1501
|
+
/**
|
|
1502
|
+
* Read the bytes of assets in this arche's asset library — for Web Audio
|
|
1503
|
+
* (`AudioContext.decodeAudioData`), canvas pixel work, or anything else a
|
|
1504
|
+
* signed URL cannot do (the plugin sandbox has no network). Read each sound
|
|
1505
|
+
* once per session and keep the decoded buffer.
|
|
1506
|
+
*
|
|
1507
|
+
* To load a set of sounds, pass the listed entries to `getMany`: up to 100
|
|
1508
|
+
* are read per call, so a whole sound set takes one or two calls. One call
|
|
1509
|
+
* per sound would trip the bridge's runaway-loop guard (more than 50 calls in
|
|
1510
|
+
* 5 seconds), which blocks every read for 10 seconds — `getBytes` calls made
|
|
1511
|
+
* together are batched for the same reason. The host page fetches each asset,
|
|
1512
|
+
* so each read costs one asset load like an `<img>` does. Unavailable in the
|
|
1513
|
+
* builder preview.
|
|
1514
|
+
*
|
|
1515
|
+
* Sounds are ready to trigger only once their buffers are decoded: wait for
|
|
1516
|
+
* the load before the first cue, and resume the `AudioContext` on the first
|
|
1517
|
+
* user gesture.
|
|
1518
|
+
*
|
|
1519
|
+
* Permissions: `entities:client_invoke` AND `assets:read`.
|
|
1520
|
+
*
|
|
1521
|
+
* @example
|
|
1522
|
+
* const { list } = useArcheAssets();
|
|
1523
|
+
* const { getMany } = useArcheAssetBytes();
|
|
1524
|
+
* const { entries } = await list({ kinds: ['audio'], tags: ['sfx:ui'] });
|
|
1525
|
+
* const ctx = new AudioContext();
|
|
1526
|
+
* const loaded = await getMany(entries);
|
|
1527
|
+
* const buffers = new Map<string, AudioBuffer>();
|
|
1528
|
+
* for (const entry of entries) {
|
|
1529
|
+
* const result = loaded[entry.assetId];
|
|
1530
|
+
* if (result instanceof Error) continue; // result.code says why
|
|
1531
|
+
* buffers.set(entry.name ?? entry.assetId, await ctx.decodeAudioData(result.bytes));
|
|
1532
|
+
* }
|
|
1533
|
+
*/
|
|
1534
|
+
function useArcheAssetBytes() {
|
|
1535
|
+
const bridge = useBridge();
|
|
1536
|
+
const getBytes = (0, react_1.useCallback)(async (asset) => {
|
|
1537
|
+
let ref;
|
|
1538
|
+
if (typeof asset === 'string') {
|
|
1539
|
+
try {
|
|
1540
|
+
ref = await bridge.request('arche_assets_get_url', { assetId: asset });
|
|
1541
|
+
}
|
|
1542
|
+
catch (err) {
|
|
1543
|
+
throw toArcheAssetError(err);
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
else {
|
|
1547
|
+
ref = asset;
|
|
1548
|
+
}
|
|
1549
|
+
return enqueueAssetByteRead(bridge, ref);
|
|
1550
|
+
}, [bridge]);
|
|
1551
|
+
const getMany = (0, react_1.useCallback)(async (assets) => {
|
|
1552
|
+
const read = await readAssetBytesBatch(bridge, assets);
|
|
1553
|
+
const out = {};
|
|
1554
|
+
for (const [assetId, result] of read)
|
|
1555
|
+
out[assetId] = result;
|
|
1556
|
+
return out;
|
|
1557
|
+
}, [bridge]);
|
|
1558
|
+
return (0, react_1.useMemo)(() => ({ getBytes, getMany }), [getBytes, getMany]);
|
|
1559
|
+
}
|
|
1560
|
+
/**
|
|
1561
|
+
* Entity ID of the arche asset bytes client-side entity — the host-side
|
|
1562
|
+
* reader behind {@link useArcheAssetBytes}. Literal here so the SDK
|
|
1563
|
+
* stays bundle-clean (no `@fias/entities` runtime dep).
|
|
1564
|
+
*
|
|
1565
|
+
* SYNC NOTE: must match `ARCHE_ASSET_BYTES_ENTITY_ID` in
|
|
1566
|
+
* `packages/entities/src/integrations/arche-asset-bytes/config.ts`. The
|
|
1567
|
+
* architecture test `sdk-entity-id-constants-match.test.ts` pins this.
|
|
1568
|
+
*/
|
|
1569
|
+
exports.ARCHE_ASSET_BYTES_ENTITY_ID = 'ent_intg_arche_asset_bytes_00000';
|
|
1570
|
+
/** A failed asset byte read (`useArcheAssetBytes`): `code` says why. */
|
|
1571
|
+
class ArcheAssetError extends Error {
|
|
1572
|
+
constructor(message, code) {
|
|
1573
|
+
super(message);
|
|
1574
|
+
this.code = code;
|
|
1575
|
+
this.name = 'ArcheAssetError';
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
exports.ArcheAssetError = ArcheAssetError;
|
|
1579
|
+
/** A listed URL with less than this many seconds left is replaced, not reused. */
|
|
1580
|
+
const ASSET_URL_MIN_REMAINING_SECONDS = 30;
|
|
1581
|
+
/** Most URLs the host reads in one call (`ARCHE_ASSET_BYTES_MAX_URLS` in the executor). */
|
|
1582
|
+
const ASSET_BYTES_MAX_URLS_PER_CALL = 100;
|
|
1583
|
+
/**
|
|
1584
|
+
* Most lapsed URLs one batch replaces with `arche_assets_get_url`, one call
|
|
1585
|
+
* each. Covers URLs that lapse in flight while staying far below the bridge's
|
|
1586
|
+
* 50-calls-in-5-seconds guard; a batch of older entries is answered
|
|
1587
|
+
* `ASSET_URL_EXPIRED` past this, so the plugin lists again (one call).
|
|
1588
|
+
*/
|
|
1589
|
+
const ASSET_URL_MAX_REFRESHES_PER_BATCH = 10;
|
|
1590
|
+
/** One `read` call: the outcome of each URL, in order. Throws when the call itself is refused. */
|
|
1591
|
+
async function readSignedUrls(bridge, signedUrls) {
|
|
1592
|
+
let response;
|
|
1593
|
+
try {
|
|
1594
|
+
response = await bridge.request('client_entity_invoke', { entityId: exports.ARCHE_ASSET_BYTES_ENTITY_ID, input: { verb: 'read', signedUrls } }, CLIENT_ENTITY_INVOKE_TIMEOUT_MS);
|
|
1595
|
+
}
|
|
1596
|
+
catch (err) {
|
|
1597
|
+
throw toArcheAssetError(err);
|
|
1598
|
+
}
|
|
1599
|
+
if ('error' in response) {
|
|
1600
|
+
throw new ArcheAssetError(response.error.message, response.error.code);
|
|
1601
|
+
}
|
|
1602
|
+
return response.results;
|
|
1603
|
+
}
|
|
1604
|
+
function fileError(result) {
|
|
1605
|
+
return result && 'error' in result
|
|
1606
|
+
? new ArcheAssetError(result.error.message, result.error.code)
|
|
1607
|
+
: new ArcheAssetError('the asset could not be read', 'ASSET_UNAVAILABLE');
|
|
1608
|
+
}
|
|
1609
|
+
/**
|
|
1610
|
+
* Read `items` in calls of up to 100 URLs, recording each outcome in `out`.
|
|
1611
|
+
* Assets that did not fit a call's byte budget beside the others go again
|
|
1612
|
+
* together until a pass reads nothing more (then each is too large on its
|
|
1613
|
+
* own). Returns the reused URLs that lapsed in flight, for a fresh URL.
|
|
1614
|
+
*/
|
|
1615
|
+
async function readInBatches(bridge, items, out) {
|
|
1616
|
+
const lapsed = [];
|
|
1617
|
+
let queue = items;
|
|
1618
|
+
while (queue.length > 0) {
|
|
1619
|
+
const overBudget = [];
|
|
1620
|
+
let readCount = 0;
|
|
1621
|
+
for (let at = 0; at < queue.length; at += ASSET_BYTES_MAX_URLS_PER_CALL) {
|
|
1622
|
+
const chunk = queue.slice(at, at + ASSET_BYTES_MAX_URLS_PER_CALL);
|
|
1623
|
+
const results = await readSignedUrls(bridge, chunk.map((item) => item.signedUrl));
|
|
1624
|
+
chunk.forEach((item, i) => {
|
|
1625
|
+
const result = results[i];
|
|
1626
|
+
if (result && !('error' in result)) {
|
|
1627
|
+
out.set(item.assetId, result);
|
|
1628
|
+
readCount += 1;
|
|
1629
|
+
}
|
|
1630
|
+
else if (result?.error.code === 'ASSET_URL_EXPIRED' && !item.refreshed) {
|
|
1631
|
+
// A reused URL can lapse before the host's fetch lands (or the
|
|
1632
|
+
// device clock runs fast); a fresh one recovers it.
|
|
1633
|
+
lapsed.push(item);
|
|
1634
|
+
}
|
|
1635
|
+
else if (result?.error.code === 'ASSET_TOO_LARGE' && chunk.length > 1) {
|
|
1636
|
+
overBudget.push(item);
|
|
1637
|
+
}
|
|
1638
|
+
else {
|
|
1639
|
+
out.set(item.assetId, fileError(result));
|
|
1640
|
+
}
|
|
1641
|
+
});
|
|
1642
|
+
}
|
|
1643
|
+
if (readCount === 0) {
|
|
1644
|
+
for (const item of overBudget) {
|
|
1645
|
+
out.set(item.assetId, new ArcheAssetError('the asset is too large to read', 'ASSET_TOO_LARGE'));
|
|
1646
|
+
}
|
|
1647
|
+
break;
|
|
1648
|
+
}
|
|
1649
|
+
queue = overBudget;
|
|
1650
|
+
}
|
|
1651
|
+
return lapsed;
|
|
1652
|
+
}
|
|
1653
|
+
/**
|
|
1654
|
+
* Read `refs` in as few host calls as possible: reuse each listed URL while it
|
|
1655
|
+
* has time left, replace a bounded number of lapsed ones, and send up to 100
|
|
1656
|
+
* URLs per call. Throws only when a call is refused as a whole.
|
|
1657
|
+
*/
|
|
1658
|
+
async function readAssetBytesBatch(bridge, refs) {
|
|
1659
|
+
const out = new Map();
|
|
1660
|
+
let refreshesLeft = ASSET_URL_MAX_REFRESHES_PER_BATCH;
|
|
1661
|
+
// A fresh URL for each lapsed item, within the batch's refresh allowance.
|
|
1662
|
+
const refresh = async (assetIds) => {
|
|
1663
|
+
const fresh = [];
|
|
1664
|
+
for (const assetId of assetIds) {
|
|
1665
|
+
if (refreshesLeft <= 0) {
|
|
1666
|
+
out.set(assetId, new ArcheAssetError('the asset URL has expired; list the assets again for fresh URLs', 'ASSET_URL_EXPIRED'));
|
|
1667
|
+
continue;
|
|
1668
|
+
}
|
|
1669
|
+
refreshesLeft -= 1;
|
|
1670
|
+
try {
|
|
1671
|
+
const asset = await bridge.request('arche_assets_get_url', { assetId });
|
|
1672
|
+
fresh.push({ assetId, signedUrl: asset.signedUrl, refreshed: true });
|
|
1673
|
+
}
|
|
1674
|
+
catch (err) {
|
|
1675
|
+
out.set(assetId, toArcheAssetError(err));
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
return fresh;
|
|
1679
|
+
};
|
|
1680
|
+
// One read per asset, whatever the caller passed twice.
|
|
1681
|
+
const unique = new Map();
|
|
1682
|
+
for (const ref of refs)
|
|
1683
|
+
if (!unique.has(ref.assetId))
|
|
1684
|
+
unique.set(ref.assetId, ref);
|
|
1685
|
+
const nowSeconds = Date.now() / 1000;
|
|
1686
|
+
const reusable = [];
|
|
1687
|
+
const stale = [];
|
|
1688
|
+
for (const ref of unique.values()) {
|
|
1689
|
+
if (ref.expiresAt - nowSeconds > ASSET_URL_MIN_REMAINING_SECONDS) {
|
|
1690
|
+
reusable.push({ assetId: ref.assetId, signedUrl: ref.signedUrl, refreshed: false });
|
|
1691
|
+
}
|
|
1692
|
+
else {
|
|
1693
|
+
stale.push(ref.assetId);
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
const pending = [...reusable, ...(await refresh(stale))];
|
|
1697
|
+
const lapsed = await readInBatches(bridge, pending, out);
|
|
1698
|
+
if (lapsed.length > 0) {
|
|
1699
|
+
await readInBatches(bridge, await refresh(lapsed.map((item) => item.assetId)), out);
|
|
1700
|
+
}
|
|
1701
|
+
return out;
|
|
1702
|
+
}
|
|
1703
|
+
const pendingAssetByteReads = new WeakMap();
|
|
1704
|
+
function enqueueAssetByteRead(bridge, ref) {
|
|
1705
|
+
let batch = pendingAssetByteReads.get(bridge);
|
|
1706
|
+
if (!batch) {
|
|
1707
|
+
const created = { refs: [], waiters: [] };
|
|
1708
|
+
pendingAssetByteReads.set(bridge, created);
|
|
1709
|
+
batch = created;
|
|
1710
|
+
void Promise.resolve().then(() => flushAssetByteReads(bridge, created));
|
|
1711
|
+
}
|
|
1712
|
+
batch.refs.push(ref);
|
|
1713
|
+
const queued = batch;
|
|
1714
|
+
return new Promise((resolve, reject) => {
|
|
1715
|
+
queued.waiters.push({ assetId: ref.assetId, resolve, reject });
|
|
1716
|
+
});
|
|
1717
|
+
}
|
|
1718
|
+
async function flushAssetByteReads(bridge, batch) {
|
|
1719
|
+
pendingAssetByteReads.delete(bridge);
|
|
1720
|
+
let results;
|
|
1721
|
+
try {
|
|
1722
|
+
results = await readAssetBytesBatch(bridge, batch.refs);
|
|
1723
|
+
}
|
|
1724
|
+
catch (err) {
|
|
1725
|
+
const failure = toArcheAssetError(err);
|
|
1726
|
+
for (const waiter of batch.waiters)
|
|
1727
|
+
waiter.reject(failure);
|
|
1728
|
+
return;
|
|
1729
|
+
}
|
|
1730
|
+
const handedOut = new Set();
|
|
1731
|
+
for (const waiter of batch.waiters) {
|
|
1732
|
+
const result = results.get(waiter.assetId) ??
|
|
1733
|
+
new ArcheAssetError('the asset could not be read', 'ASSET_UNAVAILABLE');
|
|
1734
|
+
if (result instanceof ArcheAssetError) {
|
|
1735
|
+
waiter.reject(result);
|
|
1736
|
+
}
|
|
1737
|
+
else if (handedOut.has(waiter.assetId)) {
|
|
1738
|
+
// Two callers asked for one asset: each gets its own buffer, because
|
|
1739
|
+
// `decodeAudioData` detaches the one it is given.
|
|
1740
|
+
waiter.resolve({ ...result, bytes: result.bytes.slice(0) });
|
|
1741
|
+
}
|
|
1742
|
+
else {
|
|
1743
|
+
handedOut.add(waiter.assetId);
|
|
1744
|
+
waiter.resolve(result);
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
function toArcheAssetError(err) {
|
|
1749
|
+
if (err instanceof ArcheAssetError)
|
|
1750
|
+
return err;
|
|
1751
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1752
|
+
const code = err?.code;
|
|
1753
|
+
if (/permission denied/i.test(message)) {
|
|
1754
|
+
return new ArcheAssetError(`${message} — useArcheAssetBytes needs both "assets:read" and "entities:client_invoke" in fias-plugin.json`, 'PERMISSION_DENIED');
|
|
1755
|
+
}
|
|
1756
|
+
if (code === 'RATE_LIMITED' || code === 'RATE_LIMIT' || /rate limit/i.test(message)) {
|
|
1757
|
+
return new ArcheAssetError(message, 'RATE_LIMIT');
|
|
1758
|
+
}
|
|
1759
|
+
if (/rapid-fire loop|runaway loop/i.test(message)) {
|
|
1760
|
+
return new ArcheAssetError(`${message} Load many assets with useArcheAssetBytes().getMany(entries) — one call per 100.`, 'RATE_LIMIT');
|
|
1761
|
+
}
|
|
1762
|
+
return new ArcheAssetError(message, 'ASSET_UNAVAILABLE');
|
|
1763
|
+
}
|
|
1487
1764
|
/**
|
|
1488
1765
|
* Entity ID of the plugin content pack client-side entity (ADR-021) — the
|
|
1489
1766
|
* host-side reader behind {@link useFiasContent}. Literal here so the SDK
|
|
@@ -1494,6 +1771,143 @@ function useArcheAssets() {
|
|
|
1494
1771
|
* architecture test `sdk-entity-id-constants-match.test.ts` pins this.
|
|
1495
1772
|
*/
|
|
1496
1773
|
exports.PLUGIN_CONTENT_ENTITY_ID = 'ent_intg_plugin_content_00000000';
|
|
1774
|
+
/**
|
|
1775
|
+
* Entity ID of the plugin local store client-side entity (ADR-031) — the
|
|
1776
|
+
* host-side store behind {@link useLocalStore}. Literal here so the SDK stays
|
|
1777
|
+
* bundle-clean (no `@fias/entities` runtime dep).
|
|
1778
|
+
*
|
|
1779
|
+
* SYNC NOTE: must match `PLUGIN_LOCAL_STORE_ENTITY_ID` in
|
|
1780
|
+
* `packages/entities/src/integrations/plugin-local-store/config.ts`. The
|
|
1781
|
+
* architecture test `sdk-entity-id-constants-match.test.ts` pins this.
|
|
1782
|
+
*/
|
|
1783
|
+
exports.PLUGIN_LOCAL_STORE_ENTITY_ID = 'ent_intg_plugin_local_store_0000';
|
|
1784
|
+
/** A failed local-store call: `code` says why. */
|
|
1785
|
+
class FiasLocalStoreError extends Error {
|
|
1786
|
+
constructor(message, code) {
|
|
1787
|
+
super(message);
|
|
1788
|
+
this.code = code;
|
|
1789
|
+
this.name = 'FiasLocalStoreError';
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
exports.FiasLocalStoreError = FiasLocalStoreError;
|
|
1793
|
+
function toLocalStoreError(err) {
|
|
1794
|
+
if (err instanceof FiasLocalStoreError)
|
|
1795
|
+
return err;
|
|
1796
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1797
|
+
// The bridge's centralized gates carry a code; the registry's own refusals
|
|
1798
|
+
// (per-entity permission, per-entity rate bucket) are plain Errors, so the
|
|
1799
|
+
// message is the fallback, never the first look.
|
|
1800
|
+
const raw = err?.code;
|
|
1801
|
+
const code = raw === 'PERMISSION_DENIED' || /permission denied/i.test(message)
|
|
1802
|
+
? 'PERMISSION_DENIED'
|
|
1803
|
+
: raw === 'RATE_LIMIT' || raw === 'RATE_LIMITED' || /rate limit/i.test(message)
|
|
1804
|
+
? 'RATE_LIMIT'
|
|
1805
|
+
: 'LOCAL_STORE_UNAVAILABLE';
|
|
1806
|
+
return new FiasLocalStoreError(message, code);
|
|
1807
|
+
}
|
|
1808
|
+
/**
|
|
1809
|
+
* Keep working data on the user's device — a recovery journal, drafts, a
|
|
1810
|
+
* cache — so a refresh, a crash or a closed tab loses nothing. Writes take
|
|
1811
|
+
* milliseconds and never touch the network: the host page keeps the data in
|
|
1812
|
+
* its own browser storage, in a space only this plugin can read, for the
|
|
1813
|
+
* signed-in user in the workspace they are working in (Personal and each org
|
|
1814
|
+
* workspace keep separate data; switching workspace switches the store).
|
|
1815
|
+
* Signing out clears it.
|
|
1816
|
+
*
|
|
1817
|
+
* This is NOT where a user's work is kept. It is best-effort device storage —
|
|
1818
|
+
* a browser may clear site data, and nothing here reaches another device —
|
|
1819
|
+
* so write the journal here and save the user's files through the Vault
|
|
1820
|
+
* (`useVaultUserDocuments().saveContent`, `upload`, or the Save dialog).
|
|
1821
|
+
*
|
|
1822
|
+
* Values: a string, bytes (`ArrayBuffer` / `Uint8Array`), or any JSON value;
|
|
1823
|
+
* `get` returns the same kind. One value ≤ 64 MB; the whole store for this
|
|
1824
|
+
* user ≤ 256 MB. Over budget, `put` throws `LOCAL_STORE_QUOTA_EXCEEDED`
|
|
1825
|
+
* rather than evicting — decide what to keep with `estimate()` and `list()`.
|
|
1826
|
+
* For a large snapshot (an image, a big document), put BYTES: a JSON value is
|
|
1827
|
+
* validated and serialized on the host page, which costs time proportional
|
|
1828
|
+
* to its size on every write, while bytes are only copied. Restore a journal
|
|
1829
|
+
* with `getMany(keys)` — one call for up to 100 entries, instead of one call
|
|
1830
|
+
* per key.
|
|
1831
|
+
*
|
|
1832
|
+
* Permission: `storage:local`.
|
|
1833
|
+
*
|
|
1834
|
+
* @example
|
|
1835
|
+
* const store = useLocalStore();
|
|
1836
|
+
* await store.put(`session/${docId}`, snapshotJson); // on every change
|
|
1837
|
+
* const saved = await store.getJson<Snapshot>(`session/${docId}`); // on load
|
|
1838
|
+
* await store.delete(`session/${docId}`); // on close
|
|
1839
|
+
*/
|
|
1840
|
+
function useLocalStore() {
|
|
1841
|
+
const bridge = useBridge();
|
|
1842
|
+
const call = (0, react_1.useCallback)(async (input) => {
|
|
1843
|
+
let response;
|
|
1844
|
+
try {
|
|
1845
|
+
response = await bridge.request('client_entity_invoke', { entityId: exports.PLUGIN_LOCAL_STORE_ENTITY_ID, input }, CLIENT_ENTITY_INVOKE_TIMEOUT_MS);
|
|
1846
|
+
}
|
|
1847
|
+
catch (err) {
|
|
1848
|
+
throw toLocalStoreError(err);
|
|
1849
|
+
}
|
|
1850
|
+
if (response?.error) {
|
|
1851
|
+
throw new FiasLocalStoreError(response.error.message, response.error.code);
|
|
1852
|
+
}
|
|
1853
|
+
return response ?? {};
|
|
1854
|
+
}, [bridge]);
|
|
1855
|
+
const get = (0, react_1.useCallback)(async (key) => (await call({ verb: 'get', key })).entry ?? null, [call]);
|
|
1856
|
+
const getText = (0, react_1.useCallback)(async (key) => {
|
|
1857
|
+
const entry = await get(key);
|
|
1858
|
+
if (!entry)
|
|
1859
|
+
return null;
|
|
1860
|
+
if (typeof entry.value !== 'string') {
|
|
1861
|
+
throw new FiasLocalStoreError(`"${key}" does not hold text`, 'LOCAL_STORE_WRONG_KIND');
|
|
1862
|
+
}
|
|
1863
|
+
return entry.value;
|
|
1864
|
+
}, [get]);
|
|
1865
|
+
const getBytes = (0, react_1.useCallback)(async (key) => {
|
|
1866
|
+
const entry = await get(key);
|
|
1867
|
+
if (!entry)
|
|
1868
|
+
return null;
|
|
1869
|
+
if (!(entry.value instanceof ArrayBuffer)) {
|
|
1870
|
+
throw new FiasLocalStoreError(`"${key}" does not hold bytes`, 'LOCAL_STORE_WRONG_KIND');
|
|
1871
|
+
}
|
|
1872
|
+
return entry.value;
|
|
1873
|
+
}, [get]);
|
|
1874
|
+
const getJson = (0, react_1.useCallback)(async (key) => {
|
|
1875
|
+
const entry = await get(key);
|
|
1876
|
+
if (!entry)
|
|
1877
|
+
return null;
|
|
1878
|
+
if (entry.value instanceof ArrayBuffer) {
|
|
1879
|
+
throw new FiasLocalStoreError(`"${key}" holds bytes, not JSON`, 'LOCAL_STORE_WRONG_KIND');
|
|
1880
|
+
}
|
|
1881
|
+
// A top-level string is always stored as text, so `put(key,
|
|
1882
|
+
// JSON.stringify(x))` reads back here the same as `put(key, x)`.
|
|
1883
|
+
if (typeof entry.value === 'string') {
|
|
1884
|
+
try {
|
|
1885
|
+
return JSON.parse(entry.value);
|
|
1886
|
+
}
|
|
1887
|
+
catch {
|
|
1888
|
+
throw new FiasLocalStoreError(`"${key}" holds text that is not JSON`, 'LOCAL_STORE_WRONG_KIND');
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
return entry.value;
|
|
1892
|
+
}, [get]);
|
|
1893
|
+
const getMany = (0, react_1.useCallback)(async (keys) => (await call({ verb: 'get_many', keys }))
|
|
1894
|
+
.entries ?? [], [call]);
|
|
1895
|
+
const put = (0, react_1.useCallback)(async (key, value) => ({
|
|
1896
|
+
sizeBytes: (await call({ verb: 'put', key, value })).sizeBytes ?? 0,
|
|
1897
|
+
}), [call]);
|
|
1898
|
+
const del = (0, react_1.useCallback)(async (key) => {
|
|
1899
|
+
await call({ verb: 'delete', key });
|
|
1900
|
+
}, [call]);
|
|
1901
|
+
const list = (0, react_1.useCallback)(async (prefix) => (await call({ verb: 'list', ...(prefix !== undefined ? { prefix } : {}) })).entries ?? [], [call]);
|
|
1902
|
+
const clear = (0, react_1.useCallback)(async () => ({
|
|
1903
|
+
deleted: (await call({ verb: 'clear' })).deleted ?? 0,
|
|
1904
|
+
}), [call]);
|
|
1905
|
+
const estimate = (0, react_1.useCallback)(async () => {
|
|
1906
|
+
const res = await call({ verb: 'estimate' });
|
|
1907
|
+
return { usedBytes: res.usedBytes ?? 0, budgetBytes: res.budgetBytes ?? 0 };
|
|
1908
|
+
}, [call]);
|
|
1909
|
+
return (0, react_1.useMemo)(() => ({ get, getMany, getText, getBytes, getJson, put, delete: del, list, clear, estimate }), [get, getMany, getText, getBytes, getJson, put, del, list, clear, estimate]);
|
|
1910
|
+
}
|
|
1497
1911
|
/** A failed content read: `code` says why, `path` names the file when there is one. */
|
|
1498
1912
|
class FiasContentError extends Error {
|
|
1499
1913
|
constructor(message, code, path) {
|
|
@@ -2130,9 +2544,11 @@ function useCommunityAssets() {
|
|
|
2130
2544
|
* mutate the source. And you are the authoritative type gate — if you cannot
|
|
2131
2545
|
* open this file, tell the user plainly rather than opening something broken.
|
|
2132
2546
|
*
|
|
2133
|
-
* To be offered as a destination at all,
|
|
2134
|
-
*
|
|
2135
|
-
*
|
|
2547
|
+
* To be offered as a destination at all, declare what you open in
|
|
2548
|
+
* `fias-plugin.json` (`"fileIntake": { "kinds": ["document"], "types": [".docx"] }`;
|
|
2549
|
+
* `types` narrows a kind to the files you really open) and have Fias approve
|
|
2550
|
+
* it; declaring this hook alone does not enrol you. A kind or type added later
|
|
2551
|
+
* stays off until it is approved too.
|
|
2136
2552
|
*/
|
|
2137
2553
|
function useArcheHandoff(onHandoff) {
|
|
2138
2554
|
const bridge = useBridge();
|