@seedprotocol/sdk 0.7.10 → 0.7.11-next.1384.g8b47246

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. package/README.md +1 -1
  2. package/dist/{Item-oPnmIehk.js → Item-Ld1iPicQ.js} +2051 -412
  3. package/dist/Item-Ld1iPicQ.js.map +1 -0
  4. package/dist/{ModelProperty-D2vJozmW.js → ModelProperty-C8gTx4pO.js} +32 -33
  5. package/dist/ModelProperty-C8gTx4pO.js.map +1 -0
  6. package/dist/{Schema-BMXRF4j9.js → Schema-BzeBUZxi.js} +19 -19
  7. package/dist/{Schema-BMXRF4j9.js.map → Schema-BzeBUZxi.js.map} +1 -1
  8. package/dist/{SchemaValidationService-CieGrj5c.js → SchemaValidationService-CW0s7V7R.js} +5 -6
  9. package/dist/{SchemaValidationService-CieGrj5c.js.map → SchemaValidationService-CW0s7V7R.js.map} +1 -1
  10. package/dist/arweaveContentTags-Bh4wCTal.js +51 -0
  11. package/dist/arweaveContentTags-Bh4wCTal.js.map +1 -0
  12. package/dist/dist/{index.node-BfiBriEq.d.ts → index.node-DV1OXCp0.d.ts} +25 -29
  13. package/dist/dist/{index.node-BfiBriEq.d.ts.map → index.node-DV1OXCp0.d.ts.map} +1 -1
  14. package/dist/dist/platform.node.d.ts +1 -1
  15. package/dist/download-Cvac_VGu.js +394 -0
  16. package/dist/download-Cvac_VGu.js.map +1 -0
  17. package/dist/{eas-FZPPqxqe.js → eas-CdhfODXL.js} +10 -11
  18. package/dist/{eas-FZPPqxqe.js.map → eas-CdhfODXL.js.map} +1 -1
  19. package/dist/ensureImageLocal-DNM6c2yD.js +176 -0
  20. package/dist/ensureImageLocal-DNM6c2yD.js.map +1 -0
  21. package/dist/{errors-CnT0fSMK.js → errors-DKxFSvb3.js} +179 -87
  22. package/dist/errors-DKxFSvb3.js.map +1 -0
  23. package/dist/{getPublishPayload-DKuEaq96.js → getPublishPayload-CLrAcZwr.js} +7 -7
  24. package/dist/{getPublishPayload-DKuEaq96.js.map → getPublishPayload-CLrAcZwr.js.map} +1 -1
  25. package/dist/{getPublishPendingDiff-CH5xLMjU.js → getPublishPendingDiff-DiPff8pS.js} +12 -13
  26. package/dist/{getPublishPendingDiff-CH5xLMjU.js.map → getPublishPendingDiff-DiPff8pS.js.map} +1 -1
  27. package/dist/{getPublishUploads-DivYpgNw.js → getPublishUploads-DFcgc27c.js} +6 -5
  28. package/dist/{getPublishUploads-DivYpgNw.js.map → getPublishUploads-DFcgc27c.js.map} +1 -1
  29. package/dist/index-9udoYObJ.js +25 -0
  30. package/dist/index-9udoYObJ.js.map +1 -0
  31. package/dist/{index-Gh42_47r.js → index-Cdm7Q7el.js} +17 -15
  32. package/dist/index-Cdm7Q7el.js.map +1 -0
  33. package/dist/{index-DXRhFg1N.js → index-CghAYA-z.js} +6 -7
  34. package/dist/index-CghAYA-z.js.map +1 -0
  35. package/dist/{index.browser-DQstVvRV.js → index.browser-D9oC-KKx.js} +41 -40
  36. package/dist/index.browser-D9oC-KKx.js.map +1 -0
  37. package/dist/{index.node-BfiBriEq.js → index.node-DV1OXCp0.js} +7 -31
  38. package/dist/index.node-DV1OXCp0.js.map +1 -0
  39. package/dist/main.js +19 -16
  40. package/dist/main.js.map +1 -1
  41. package/dist/{modelPropertyRecordSchemas-DEe-lYhn.js → modelPropertyRecordSchemas-se4QMXSi.js} +9 -12
  42. package/dist/{modelPropertyRecordSchemas-DEe-lYhn.js.map → modelPropertyRecordSchemas-se4QMXSi.js.map} +1 -1
  43. package/dist/node.js +6 -7
  44. package/dist/node.js.map +1 -1
  45. package/dist/platform.browser.js +7 -8
  46. package/dist/platform.browser.js.map +1 -1
  47. package/dist/platform.node.js +1 -1
  48. package/dist/{property-iNW1N3vm.js → property-Broz_CWj.js} +8 -11
  49. package/dist/property-Broz_CWj.js.map +1 -0
  50. package/dist/{publishGraph-CRn_N3NT.js → publishGraph-DnWoWZRP.js} +4 -4
  51. package/dist/{publishGraph-CRn_N3NT.js.map → publishGraph-DnWoWZRP.js.map} +1 -1
  52. package/dist/{readHtmlBodyForStorageSeed-B7HgPmq6.js → readHtmlBodyForStorageSeed-FvwYPn4m.js} +9 -11
  53. package/dist/{readHtmlBodyForStorageSeed-B7HgPmq6.js.map → readHtmlBodyForStorageSeed-FvwYPn4m.js.map} +1 -1
  54. package/dist/src/Item/Item.d.ts.map +1 -1
  55. package/dist/src/ItemProperty/ItemProperty.d.ts.map +1 -1
  56. package/dist/src/ItemProperty/service/actors/initialize.d.ts.map +1 -1
  57. package/dist/src/Model/index.d.ts +0 -2
  58. package/dist/src/Model/index.d.ts.map +1 -1
  59. package/dist/src/browser/helpers/FileManager.d.ts +1 -3
  60. package/dist/src/browser/helpers/FileManager.d.ts.map +1 -1
  61. package/dist/src/browser/workers/FileDownloader.d.ts +1 -0
  62. package/dist/src/browser/workers/FileDownloader.d.ts.map +1 -1
  63. package/dist/src/browser/workers/ImageResizer.d.ts +1 -0
  64. package/dist/src/browser/workers/ImageResizer.d.ts.map +1 -1
  65. package/dist/src/client/actors/addModelsToDb.d.ts.map +1 -1
  66. package/dist/src/client/actors/saveAppState.d.ts.map +1 -1
  67. package/dist/src/client/actors/saveConfig.d.ts.map +1 -1
  68. package/dist/src/db/read/metadataProperty.d.ts +1 -1
  69. package/dist/src/db/read/metadataProperty.d.ts.map +1 -1
  70. package/dist/src/db/read/resolveModelRecord.d.ts +0 -20
  71. package/dist/src/db/read/resolveModelRecord.d.ts.map +1 -1
  72. package/dist/src/db/write/createMetadata.d.ts +1 -8
  73. package/dist/src/db/write/createMetadata.d.ts.map +1 -1
  74. package/dist/src/db/write/removeLocalCopiesForAddresses.d.ts +0 -5
  75. package/dist/src/db/write/removeLocalCopiesForAddresses.d.ts.map +1 -1
  76. package/dist/src/db/write/updateItemPropertyValue.d.ts.map +1 -1
  77. package/dist/src/events/files/download.d.ts.map +1 -1
  78. package/dist/src/events/item/syncDbWithEas.d.ts.map +1 -1
  79. package/dist/src/helpers/easPropertyCanonical.d.ts +1 -1
  80. package/dist/src/helpers/easPropertyCanonical.d.ts.map +1 -1
  81. package/dist/src/helpers/entity/entityLifecycle.d.ts +0 -2
  82. package/dist/src/helpers/entity/entityLifecycle.d.ts.map +1 -1
  83. package/dist/src/helpers/environment.d.ts +0 -13
  84. package/dist/src/helpers/environment.d.ts.map +1 -1
  85. package/dist/src/helpers/index.d.ts +0 -4
  86. package/dist/src/helpers/index.d.ts.map +1 -1
  87. package/dist/src/helpers/itemStorageFile.d.ts +0 -11
  88. package/dist/src/helpers/itemStorageFile.d.ts.map +1 -1
  89. package/dist/src/helpers/propertySchemaUid.d.ts +7 -5
  90. package/dist/src/helpers/propertySchemaUid.d.ts.map +1 -1
  91. package/dist/src/helpers/storageDir.d.ts +0 -2
  92. package/dist/src/helpers/storageDir.d.ts.map +1 -1
  93. package/dist/src/node/db/Db.d.ts +0 -2
  94. package/dist/src/node/db/Db.d.ts.map +1 -1
  95. package/dist/src/node/helpers/FileManager.d.ts +0 -2
  96. package/dist/src/node/helpers/FileManager.d.ts.map +1 -1
  97. package/dist/src/node/index.d.ts +2 -2
  98. package/dist/src/node/index.d.ts.map +1 -1
  99. package/dist/src/outbox/index.d.ts +5 -5
  100. package/dist/src/outbox/index.d.ts.map +1 -1
  101. package/dist/{syncDbWithEas-FuyqoEKk.js → syncDbWithEas-Dj_8VqM7.js} +24 -23
  102. package/dist/syncDbWithEas-Dj_8VqM7.js.map +1 -0
  103. package/dist/tabEvents-BnBzIdEi.js +1408 -0
  104. package/dist/tabEvents-BnBzIdEi.js.map +1 -0
  105. package/dist/{tolerantWebAccess-zEseJ2zT.js → tolerantWebAccess-CNfvmR3m.js} +8 -9
  106. package/dist/{tolerantWebAccess-zEseJ2zT.js.map → tolerantWebAccess-CNfvmR3m.js.map} +1 -1
  107. package/dist/{updateSeedRevokedAt-B9JRwr5T.js → updateSeedRevokedAt-D-3SLW-D.js} +3 -2
  108. package/dist/{updateSeedRevokedAt-B9JRwr5T.js.map → updateSeedRevokedAt-D-3SLW-D.js.map} +1 -1
  109. package/package.json +7 -12
  110. package/dist/Item-oPnmIehk.js.map +0 -1
  111. package/dist/ModelProperty-D2vJozmW.js.map +0 -1
  112. package/dist/errors-CnT0fSMK.js.map +0 -1
  113. package/dist/index-CZMWKXLM.js +0 -27
  114. package/dist/index-CZMWKXLM.js.map +0 -1
  115. package/dist/index-DXRhFg1N.js.map +0 -1
  116. package/dist/index-Gh42_47r.js.map +0 -1
  117. package/dist/index.browser-DQstVvRV.js.map +0 -1
  118. package/dist/index.node-BfiBriEq.js.map +0 -1
  119. package/dist/property-iNW1N3vm.js.map +0 -1
  120. package/dist/src/db/read/getItemProperty.d.ts +0 -6
  121. package/dist/src/db/read/getItemProperty.d.ts.map +0 -1
  122. package/dist/src/db/read/isSeedRevoked.d.ts +0 -7
  123. package/dist/src/db/read/isSeedRevoked.d.ts.map +0 -1
  124. package/dist/src/db/write/saveMetadata.d.ts +0 -3
  125. package/dist/src/db/write/saveMetadata.d.ts.map +0 -1
  126. package/dist/src/db/write/updateMetadata.d.ts +0 -13
  127. package/dist/src/db/write/updateMetadata.d.ts.map +0 -1
  128. package/dist/src/types/fileManager.d.ts +0 -13
  129. package/dist/src/types/fileManager.d.ts.map +0 -1
  130. package/dist/syncDbWithEas-FuyqoEKk.js.map +0 -1
  131. package/dist/tabEvents-DT5ez6io.js +0 -3830
  132. package/dist/tabEvents-DT5ez6io.js.map +0 -1
@@ -1,3830 +0,0 @@
1
- import debug from 'debug';
2
- import { B as BaseDb, t as schemas, d as models, j as modelSchemas, v as versions, s as seeds, l as properties, p as pendingWrites, a as appState, w as BaseFileManager, m as metadata, g as entityChanges } from './BaseFileManager-DLou1TZS.js';
3
- import { eq, getTableColumns, and, inArray, isNotNull, desc, or, sql, gt, lt } from 'drizzle-orm';
4
- import { waitFor } from 'xstate';
5
- import { DEFAULT_NAME_MATCH_LEVEL, isNameMatchLevel, compareNameMatchTiers, VERSION_SCHEMA_UID, localNamesInBestMatchTier, easName, BaseQueryClient, BaseEasClient, GET_FILES_METADATA } from '@seedprotocol/eas';
6
- import { alias } from 'drizzle-orm/sqlite-core';
7
- import 'pluralize';
8
- import { Type } from '@sinclair/typebox';
9
- import { ensureReadGatewaySelected, getArweaveReadBaseUrls, BaseArweaveClient, getTransactionTagsByIds } from '@seedprotocol/arweave';
10
- import { customAlphabet } from 'nanoid';
11
- import { alphanumeric } from 'nanoid-dictionary';
12
- import { sha256 } from '@noble/hashes/sha256';
13
- import { sha3_256 } from '@noble/hashes/sha3';
14
- import { bytesToHex } from '@noble/hashes/utils';
15
- import { throttle } from 'lodash-es';
16
-
17
- var MachineIds;
18
- (function (MachineIds) {
19
- MachineIds["CLIENT_MANAGER"] = "@seedSdk/clientManager";
20
- MachineIds["EAS_SYNC_MANAGER"] = "@seedSdk/easSyncManager";
21
- MachineIds["GLOBAL"] = "@seedSdk/global";
22
- MachineIds["INTERNAL"] = "@seedSdk/internal";
23
- MachineIds["EAS"] = "@seedSdk/eas";
24
- MachineIds["ITEM"] = "@seedSdk/item";
25
- MachineIds["ALL_ITEMS"] = "@seedSdk/allItems";
26
- MachineIds["MODEL"] = "@seedSdk/model";
27
- MachineIds["FILE_SYSTEM"] = "@seedSdk/fileSystem";
28
- })(MachineIds || (MachineIds = {}));
29
- var ClientManagerState;
30
- (function (ClientManagerState) {
31
- ClientManagerState["UNINITIALIZED"] = "uninitialized";
32
- ClientManagerState["PLATFORM_CLASSES_INIT"] = "platformClassesInit";
33
- ClientManagerState["FILE_SYSTEM_INIT"] = "fileSystemInit";
34
- ClientManagerState["DB_INIT"] = "dbInit";
35
- ClientManagerState["SAVE_CONFIG"] = "saveConfig";
36
- ClientManagerState["ADD_MODELS_TO_DB"] = "addModelsToDb";
37
- ClientManagerState["PROCESS_SCHEMA_FILES"] = "processSchemaFiles";
38
- ClientManagerState["IDLE"] = "idle";
39
- ClientManagerState["INIT_FAILED"] = "initFailed";
40
- })(ClientManagerState || (ClientManagerState = {}));
41
- var ClientManagerEvents;
42
- (function (ClientManagerEvents) {
43
- ClientManagerEvents["UPDATE_CONTEXT"] = "updateContext";
44
- ClientManagerEvents["SAVE_APP_STATE_SUCCESS"] = "saveAppStateSuccess";
45
- ClientManagerEvents["SAVE_APP_STATE_ERROR"] = "saveAppStateError";
46
- ClientManagerEvents["SET_ADDRESSES"] = "setAddresses";
47
- })(ClientManagerEvents || (ClientManagerEvents = {}));
48
-
49
- // Set by ClientManager when it loads. Importing it here would pull in most of the SDK, and
50
- // readiness is read from inside that graph (entity loads, downloads).
51
- let getClientActor;
52
- /** @internal ClientManager registers the getter for its current actor (init after stop replaces it). */
53
- function registerClientActor(getter) {
54
- getClientActor = getter;
55
- }
56
- // Client states in which the app database is open: dbInit finishes before saveConfig starts.
57
- const DB_OPEN_STATES = new Set([
58
- ClientManagerState.SAVE_CONFIG,
59
- ClientManagerState.PROCESS_SCHEMA_FILES,
60
- ClientManagerState.ADD_MODELS_TO_DB,
61
- ClientManagerState.IDLE,
62
- ]);
63
- /** Whether a clientManager snapshot is a running client that finished init. */
64
- function isClientReadySnapshot(snapshot) {
65
- return snapshot.status === 'active' && snapshot.value === ClientManagerState.IDLE;
66
- }
67
- /**
68
- * Whether `client.init` finished and the app database is open. False before init, while it runs,
69
- * and after `client.stop()`.
70
- */
71
- function isClientReady() {
72
- const actor = getClientActor?.();
73
- return !!actor && isClientReadySnapshot(actor.getSnapshot()) && BaseDb.isAppDbReady();
74
- }
75
- /**
76
- * The app database, once client init has opened it. Waits up to `timeoutMs` for init to get that
77
- * far; returns undefined when it doesn't (no init, a failed or stopped client).
78
- */
79
- async function whenDbReady(timeoutMs = 30000) {
80
- if (BaseDb.isAppDbReady())
81
- return BaseDb.getAppDb();
82
- const actor = getClientActor?.();
83
- if (!actor)
84
- return undefined;
85
- try {
86
- await waitFor(actor, (snapshot) => DB_OPEN_STATES.has(snapshot.value), {
87
- timeout: timeoutMs,
88
- });
89
- }
90
- catch {
91
- return undefined;
92
- }
93
- return BaseDb.isAppDbReady() ? BaseDb.getAppDb() : undefined;
94
- }
95
-
96
- const refreshers = new Set();
97
- function addCachedPropertyRefresher(fn) {
98
- refreshers.add(fn);
99
- }
100
- /** Give cached instances of these properties their rows' current definitions. */
101
- async function refreshCachedProperties(propertyFileIds) {
102
- if (propertyFileIds.length === 0)
103
- return;
104
- for (const refresher of refreshers)
105
- await refresher(propertyFileIds);
106
- }
107
-
108
- const level = (value) => (isNameMatchLevel(value) ? value : undefined);
109
- const tighter = (a, b) => a === undefined || compareNameMatchTiers(b, a) < 0 ? b : a;
110
- /** The levels in one schema file for one of its models, or undefined when the file lacks the model. */
111
- const modelLevelsFromFile = (file, modelName) => {
112
- const modelDef = file.models?.[modelName];
113
- if (!modelDef)
114
- return undefined;
115
- const model = level(modelDef.nameMatch) ?? level(file.nameMatch) ?? DEFAULT_NAME_MATCH_LEVEL;
116
- const properties = new Map();
117
- for (const [propertyName, def] of Object.entries(modelDef.properties ?? {})) {
118
- properties.set(propertyName, level(def?.nameMatch) ?? model);
119
- }
120
- return { model, properties };
121
- };
122
- /**
123
- * Builds NameMatchLevels from the schema files that own each model row. Exported for tests;
124
- * use {@link nameMatchLevels}.
125
- */
126
- const nameMatchLevelsFromRows = (rows) => {
127
- const byFileId = new Map();
128
- const byName = new Map();
129
- const parsed = new Map();
130
- for (const row of rows) {
131
- if (!row.schemaData)
132
- continue;
133
- if (!parsed.has(row.schemaData)) {
134
- try {
135
- parsed.set(row.schemaData, JSON.parse(row.schemaData));
136
- }
137
- catch {
138
- parsed.set(row.schemaData, undefined);
139
- }
140
- }
141
- const file = parsed.get(row.schemaData);
142
- const levels = file && modelLevelsFromFile(file, row.modelName);
143
- if (!levels)
144
- continue;
145
- if (row.modelFileId)
146
- byFileId.set(row.modelFileId, levels);
147
- const seen = byName.get(row.modelName);
148
- if (!seen) {
149
- byName.set(row.modelName, { model: levels.model, properties: new Map(levels.properties) });
150
- continue;
151
- }
152
- seen.model = tighter(seen.model, levels.model);
153
- for (const [name, value] of levels.properties)
154
- seen.properties.set(name, tighter(seen.properties.get(name), value));
155
- }
156
- return {
157
- model: (modelName) => byName.get(modelName)?.model ?? DEFAULT_NAME_MATCH_LEVEL,
158
- property: (modelName, propertyName, modelFileId) => {
159
- const levels = (modelFileId ? byFileId.get(modelFileId) : undefined) ?? byName.get(modelName);
160
- return levels?.properties.get(propertyName) ?? levels?.model ?? DEFAULT_NAME_MATCH_LEVEL;
161
- },
162
- };
163
- };
164
- const DEFAULT_LEVELS = {
165
- model: () => DEFAULT_NAME_MATCH_LEVEL,
166
- property: () => DEFAULT_NAME_MATCH_LEVEL,
167
- };
168
- let current;
169
- let loading;
170
- /** Reads the levels from the schema files stored with each model's schema row (schemas.schema_data). */
171
- const loadNameMatchLevels = async (db = BaseDb.getAppDb()) => {
172
- if (!db)
173
- return DEFAULT_LEVELS;
174
- const load = (async () => {
175
- const rows = (await db
176
- .select({ modelName: models.name, modelFileId: models.schemaFileId, schemaData: schemas.schemaData })
177
- .from(models)
178
- .innerJoin(modelSchemas, eq(models.id, modelSchemas.modelId))
179
- .innerJoin(schemas, eq(modelSchemas.schemaId, schemas.id)));
180
- return nameMatchLevelsFromRows(rows);
181
- })();
182
- loading = load;
183
- try {
184
- const levels = await load;
185
- if (loading === load) {
186
- current = levels;
187
- previous = undefined;
188
- }
189
- return levels;
190
- }
191
- finally {
192
- if (loading === load)
193
- loading = undefined;
194
- }
195
- };
196
- /** The levels, loaded once and reused until a schema write clears them. */
197
- const nameMatchLevels = async (db = BaseDb.getAppDb()) => current ?? loading ?? loadNameMatchLevels(db);
198
- /**
199
- * The last loaded levels, for synchronous callers (Model.findByModelType). Init loads them before
200
- * it reports ready. After a schema write clears them, this returns the levels from before the
201
- * write until the reload that clear starts finishes.
202
- */
203
- const currentNameMatchLevels = () => current ?? previous ?? DEFAULT_LEVELS;
204
- let previous;
205
- let reloadScheduled = false;
206
- /**
207
- * Called when a schema row or its models are written. The next {@link nameMatchLevels} read
208
- * reloads, and a reload starts on the next tick (one per burst of writes) for synchronous callers.
209
- */
210
- const clearNameMatchLevels = () => {
211
- if (current)
212
- previous = current;
213
- current = undefined;
214
- loading = undefined;
215
- if (reloadScheduled)
216
- return;
217
- reloadScheduled = true;
218
- setTimeout(() => {
219
- reloadScheduled = false;
220
- if (current || loading || !BaseDb.isAppDbReady())
221
- return;
222
- loadNameMatchLevels().catch(() => {
223
- // The next nameMatchLevels() read retries.
224
- });
225
- }, 0);
226
- };
227
- /**
228
- * `nameMatch` values in a schema file that aren't a level. They are ignored (the next level up
229
- * applies); schema loads warn about them.
230
- */
231
- const invalidNameMatchValues = (file) => {
232
- const problems = [];
233
- const check = (where, value) => {
234
- if (value !== undefined && !isNameMatchLevel(value)) {
235
- problems.push(`${where} nameMatch ${JSON.stringify(value)} is not 'exact', 'normalized' or 'fuzzy'`);
236
- }
237
- };
238
- check('schema', file.nameMatch);
239
- for (const [modelName, modelDef] of Object.entries(file.models ?? {})) {
240
- check(modelName, modelDef?.nameMatch);
241
- for (const [propertyName, def] of Object.entries(modelDef?.properties ?? {})) {
242
- check(`${modelName}.${propertyName}`, def?.nameMatch);
243
- }
244
- }
245
- return problems;
246
- };
247
- /** Warns about {@link invalidNameMatchValues}. */
248
- const warnOnInvalidNameMatch = (file) => {
249
- const problems = invalidNameMatchValues(file);
250
- for (const problem of problems) {
251
- console.warn(`[seed] schema ${file.metadata?.name ?? ''}: ${problem}; it is ignored.`);
252
- }
253
- return problems;
254
- };
255
-
256
- // Define zero constants ourselves since they're not exported from eas-sdk in newer versions
257
- const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';
258
- const ZERO_BYTES = '0x';
259
- const ZERO_BYTES32 = '0x0000000000000000000000000000000000000000000000000000000000000000';
260
- const INTERNAL_DATA_TYPES = {
261
- Text: {
262
- eas: 'string',
263
- },
264
- // ADR 0010: decimal strings, so negatives, fractions and values above 255 publish.
265
- Number: {
266
- eas: 'string',
267
- },
268
- Image: {
269
- eas: 'bytes32',
270
- },
271
- Relation: {
272
- eas: 'bytes32',
273
- },
274
- List: {
275
- eas: 'bytes32[]',
276
- },
277
- File: {
278
- eas: 'bytes32',
279
- },
280
- Json: {
281
- eas: 'bytes32',
282
- },
283
- Blob: {
284
- eas: 'bytes32',
285
- },
286
- Boolean: {
287
- eas: 'bool',
288
- },
289
- Date: {
290
- eas: 'uint256',
291
- },
292
- Html: {
293
- eas: 'bytes32',
294
- },
295
- };
296
- /**
297
- * Internal property names ending in `Id` that are names in their own right: never stripped to a
298
- * base name, and never given an `Id` suffix in metadata.
299
- */
300
- const PROPERTY_NAMES_EXEMPT_FROM_ID_SUFFIX_STRIP = new Set(['storageTransactionId', 'transactionId']);
301
- const INTERNAL_PROPERTY_NAMES = [
302
- 'localId',
303
- 'uid',
304
- 'seedLocalId',
305
- 'seedUid',
306
- 'schemaUid',
307
- 'attestationCreatedAt',
308
- 'attestationRaw',
309
- 'createdAt',
310
- 'updatedAt',
311
- 'lastVersionPublishedAt',
312
- 'latestVersionLocalId',
313
- 'latestVersionUid',
314
- 'lastLocalUpdateAt',
315
- 'modelName',
316
- 'refSeedType',
317
- 'refValueType',
318
- 'refResolvedValue',
319
- 'refResolvedDisplayValue',
320
- 'type',
321
- // Image
322
- 'src',
323
- 'alt',
324
- //
325
- 'versionLocalId',
326
- 'versionsCount',
327
- 'versionUid',
328
- '_markedForDeletion',
329
- // SDK-internal: wallet address of attester; never publish as property attestation
330
- 'publisher',
331
- // SDK-internal: Unix timestamp when attestations were revoked; never a schema field
332
- 'revokedAt',
333
- ];
334
- /** @deprecated The UID is chain-independent. Use {@link VERSION_SCHEMA_UID}. */
335
- const VERSION_SCHEMA_UID_OPTIMISM_SEPOLIA = VERSION_SCHEMA_UID;
336
- const defaultAttestationData = {
337
- recipient: ZERO_ADDRESS,
338
- revocable: true,
339
- value: BigInt(0),
340
- refUID: ZERO_BYTES32,
341
- expirationTime: BigInt(0),
342
- data: ZERO_BYTES,
343
- };
344
- var ImageSize;
345
- (function (ImageSize) {
346
- ImageSize[ImageSize["EXTRA_SMALL"] = 480] = "EXTRA_SMALL";
347
- ImageSize[ImageSize["SMALL"] = 760] = "SMALL";
348
- ImageSize[ImageSize["MEDIUM"] = 1024] = "MEDIUM";
349
- ImageSize[ImageSize["LARGE"] = 1440] = "LARGE";
350
- ImageSize[ImageSize["EXTRA_LARGE"] = 1920] = "EXTRA_LARGE";
351
- })(ImageSize || (ImageSize = {}));
352
- const CLIENT_NOT_INITIALIZED = 'ClientManager is not initialized. Please call init() first.';
353
- var SeedModels;
354
- (function (SeedModels) {
355
- SeedModels["Seed"] = "Seed";
356
- SeedModels["Metadata"] = "Metadata";
357
- SeedModels["Version"] = "Version";
358
- })(SeedModels || (SeedModels = {}));
359
- // Internal SDK schema that should not be created in app's files directory
360
- const SEED_PROTOCOL_SCHEMA_NAME = 'Seed Protocol';
361
- const INTERNAL_SCHEMA_IDS = ['SEEDPROTOCOL'];
362
- /** Fired, in every tab, after `syncDbWithEas` persists EAS seeds, versions and metadata to SQLite. */
363
- const EAS_SEED_DATA_SYNCED_TO_DB_EVENT = 'easSeedDataSyncedToDb';
364
- /**
365
- * Check if a schema is an internal SDK schema that should not be created in app files
366
- */
367
- function isInternalSchema(schemaName, schemaId) {
368
- return schemaName === SEED_PROTOCOL_SCHEMA_NAME ||
369
- (schemaId !== undefined && INTERNAL_SCHEMA_IDS.includes(schemaId));
370
- }
371
- /**
372
- * Model ids (schemaFileId) of the internal Seed Protocol storage models. Storage seeds are always
373
- * these models, even if a user schema also defines a model named Image/File/Html.
374
- */
375
- const INTERNAL_STORAGE_MODEL_FILE_IDS = {
376
- image: 'SEEDPROTOCOL_IMAGE',
377
- file: 'SEEDPROTOCOL_FILE',
378
- html: 'SEEDPROTOCOL_HTML',
379
- };
380
-
381
- /**
382
- * SQLite builds before 3.32 (and builds that keep the old default) allow at most 999 bound
383
- * parameters per statement (SQLITE_MAX_VARIABLE_NUMBER). sqlite-wasm and libsql allow 32766, but a
384
- * statement that binds one parameter per synced item still has to stay under 999 to work everywhere.
385
- */
386
- const SQLITE_DEFAULT_MAX_VARIABLE_NUMBER = 999;
387
- /**
388
- * Values per `IN (...)` list. Leaves room under {@link SQLITE_DEFAULT_MAX_VARIABLE_NUMBER} for the
389
- * statement's other parameters (a second list, `SET` values, other conditions).
390
- */
391
- const IN_LIST_BATCH = 400;
392
- /**
393
- * Rows per multi-row INSERT into `table` so the statement binds at most
394
- * {@link SQLITE_DEFAULT_MAX_VARIABLE_NUMBER} parameters even with every column set.
395
- */
396
- const rowsPerInsert = (table) => Math.max(1, Math.floor(SQLITE_DEFAULT_MAX_VARIABLE_NUMBER / Object.keys(getTableColumns(table)).length));
397
- /** Splits `values` into chunks of at most `size` (one empty list gives no chunks). */
398
- const chunkValues = (values, size = IN_LIST_BATCH) => {
399
- if (!Number.isInteger(size) || size < 1) {
400
- throw new Error(`chunkValues: size must be a positive integer, got ${size}`);
401
- }
402
- const chunks = [];
403
- for (let i = 0; i < values.length; i += size) {
404
- chunks.push(values.slice(i, i + size));
405
- }
406
- return chunks;
407
- };
408
- /**
409
- * Runs `query` once per chunk of `values` (see {@link chunkValues}), one after another, and
410
- * concatenates the rows. Use it for `inArray(column, values)` reads whose list can be large.
411
- */
412
- const selectInBatches = async (values, query, size = IN_LIST_BATCH) => {
413
- const rows = [];
414
- for (const chunk of chunkValues(values, size)) {
415
- rows.push(...(await query(chunk)));
416
- }
417
- return rows;
418
- };
419
- /** Runs `write` once per chunk of `values`, one after another (deletes / updates with an `IN` list). */
420
- const writeInBatches = async (values, write, size = IN_LIST_BATCH) => {
421
- for (const chunk of chunkValues(values, size)) {
422
- await write(chunk);
423
- }
424
- };
425
-
426
- /**
427
- * The local seed row for each seed attestation uid. A uid recorded on a seed's versions
428
- * (`versions.seed_uid`) belongs to that seed: its current uid, or an old one from before it was
429
- * unpublished and published again (the seed row then holds the new uid, the old version rows keep
430
- * the old one). A uid no version records resolves by `seeds.uid`. Uids with no local seed are left
431
- * out of the map.
432
- *
433
- * Going through versions first also resolves a republished seed's old uid to the item when an
434
- * older sync left a second, version-less seed row with that uid (migration 0024 removes those).
435
- */
436
- const findSeedRowsByUid = async (uids,
437
- /** The database or transaction to read; the app database by default. */
438
- db = BaseDb.getAppDb()) => {
439
- const result = new Map();
440
- const appDb = db;
441
- const wanted = [...new Set(uids.filter(Boolean))];
442
- if (!appDb || wanted.length === 0)
443
- return result;
444
- const owners = await selectInBatches(wanted, (chunk) => appDb
445
- .selectDistinct({ seedUid: versions.seedUid, seedLocalId: versions.seedLocalId })
446
- .from(versions)
447
- .where(and(inArray(versions.seedUid, chunk), isNotNull(versions.seedLocalId))));
448
- const ownerLocalIds = [...new Set(owners.map((o) => o.seedLocalId))];
449
- const ownerRows = await selectInBatches(ownerLocalIds, (chunk) => appDb.select().from(seeds).where(inArray(seeds.localId, chunk)));
450
- const ownerByLocalId = new Map(ownerRows.map((row) => [row.localId, row]));
451
- for (const { seedUid, seedLocalId } of owners) {
452
- const row = ownerByLocalId.get(seedLocalId);
453
- if (!seedUid || !row)
454
- continue;
455
- // Versions of two seeds recording one uid shouldn't happen; the seed that holds it wins.
456
- const current = result.get(seedUid);
457
- if (!current || (current.uid !== seedUid && row.uid === seedUid))
458
- result.set(seedUid, row);
459
- }
460
- const rest = wanted.filter((uid) => !result.has(uid));
461
- const byUid = await selectInBatches(rest, (chunk) => appDb.select().from(seeds).where(inArray(seeds.uid, chunk)));
462
- for (const row of byUid) {
463
- if (row.uid && !result.has(row.uid))
464
- result.set(row.uid, row);
465
- }
466
- return result;
467
- };
468
-
469
- /**
470
- * The local seed row for an item known by local id, seed uid or both. The local id wins: it names
471
- * one row, while a uid can be an old one the seed held before it was unpublished and published
472
- * again. A uid (when the local id is absent or matches nothing) resolves as
473
- * {@link findSeedRowsByUid} resolves it: the seed whose versions record it (`versions.seed_uid`),
474
- * else the row with that `seeds.uid`. The zero uid matches nothing.
475
- */
476
- const findSeedRow = async ({ localId, uid },
477
- /** The database or transaction to read; the app database by default. */
478
- db = BaseDb.getAppDb()) => {
479
- if (!db)
480
- return undefined;
481
- if (localId) {
482
- const [row] = await db.select().from(seeds).where(eq(seeds.localId, localId)).limit(1);
483
- if (row?.localId)
484
- return row;
485
- }
486
- if (!uid || uid === ZERO_BYTES32)
487
- return undefined;
488
- const row = (await findSeedRowsByUid([uid], db)).get(uid);
489
- return row?.localId ? row : undefined;
490
- };
491
-
492
- var findSeedRow$1 = /*#__PURE__*/Object.freeze({
493
- __proto__: null,
494
- findSeedRow: findSeedRow
495
- });
496
-
497
- /**
498
- * Thrown when a model name exists in more than one schema and nothing says which one is meant.
499
- * Model names are only unique within a schema; pass `schemaName` (or `modelFileId`) to choose.
500
- */
501
- class AmbiguousModelError extends Error {
502
- constructor(modelName, schemaNames) {
503
- const names = [...new Set(schemaNames)].sort();
504
- super(`Model "${modelName}" exists in schemas ${names.map((n) => `"${n}"`).join(', ')}; ` +
505
- `pass schemaName (or modelFileId) to choose one.`);
506
- this.name = 'AmbiguousModelError';
507
- this.modelName = modelName;
508
- this.schemaNames = names;
509
- }
510
- }
511
-
512
- const columns = {
513
- id: models.id,
514
- name: models.name,
515
- schemaFileId: models.schemaFileId,
516
- };
517
- /**
518
- * Resolve a models row from a model name plus whatever scope is known, most specific first:
519
- * modelFileId, modelId, propertyFileId, then schemaId/schemaName + name, then the name alone (not
520
- * with `strict` and a schema scope). The name alone is only accepted when it is unambiguous;
521
- * otherwise this throws AmbiguousModelError.
522
- */
523
- const resolveModelRecord = async (modelName, scope = {}, db = BaseDb.getAppDb()) => {
524
- if (!db)
525
- return undefined;
526
- if (scope.modelFileId) {
527
- const rows = await db
528
- .select(columns)
529
- .from(models)
530
- .where(eq(models.schemaFileId, scope.modelFileId))
531
- .limit(1);
532
- if (rows.length > 0)
533
- return rows[0];
534
- }
535
- if (scope.modelId) {
536
- const rows = await db.select(columns).from(models).where(eq(models.id, scope.modelId)).limit(1);
537
- if (rows.length > 0)
538
- return rows[0];
539
- }
540
- if (scope.propertyFileId) {
541
- const rows = await db
542
- .select(columns)
543
- .from(properties)
544
- .innerJoin(models, eq(properties.modelId, models.id))
545
- .where(eq(properties.schemaFileId, scope.propertyFileId))
546
- .limit(1);
547
- if (rows.length > 0)
548
- return rows[0];
549
- }
550
- if (!modelName)
551
- return undefined;
552
- if (scope.schemaId || scope.schemaName) {
553
- const rows = await db
554
- .select(columns)
555
- .from(models)
556
- .innerJoin(modelSchemas, eq(models.id, modelSchemas.modelId))
557
- .innerJoin(schemas, eq(modelSchemas.schemaId, schemas.id))
558
- .where(and(eq(models.name, modelName), scope.schemaId ? eq(schemas.id, scope.schemaId) : eq(schemas.name, scope.schemaName)))
559
- .limit(1);
560
- if (rows.length > 0)
561
- return rows[0];
562
- if (scope.strict)
563
- return undefined;
564
- }
565
- return resolveModelRecordByNameOnly(modelName, db);
566
- };
567
- /**
568
- * Name of the schema a models row is linked to (the newest version's when several are), or
569
- * undefined for an unlinked stub.
570
- */
571
- const getModelSchemaName = async (modelId, db = BaseDb.getAppDb()) => {
572
- if (!db)
573
- return undefined;
574
- const rows = await db
575
- .select({ schemaName: schemas.name })
576
- .from(modelSchemas)
577
- .innerJoin(schemas, eq(modelSchemas.schemaId, schemas.id))
578
- .where(eq(modelSchemas.modelId, modelId))
579
- .orderBy(desc(schemas.version))
580
- .limit(1);
581
- return rows[0]?.schemaName ?? undefined;
582
- };
583
- /** The model a property row (by its schemaFileId) belongs to, and the property's data type. */
584
- const resolvePropertyModelRecord = async (propertyFileId, db = BaseDb.getAppDb()) => {
585
- if (!db || !propertyFileId)
586
- return undefined;
587
- const rows = await db
588
- .select({ ...columns, dataType: properties.dataType })
589
- .from(properties)
590
- .innerJoin(models, eq(properties.modelId, models.id))
591
- .where(eq(properties.schemaFileId, propertyFileId))
592
- .limit(1);
593
- const row = rows[0];
594
- return row ? { id: row.id, name: row.name, schemaFileId: row.schemaFileId, dataType: row.dataType ?? '' } : undefined;
595
- };
596
- /**
597
- * Name-only resolution. Rows linked to a schema win over unlinked stubs (null-schemaFileId rows
598
- * from ref resolution); if more than one linked row has the name it is ambiguous and this throws.
599
- * `label` names the lookup in the error (defaults to the model name).
600
- */
601
- const resolveModelRecordByNameOnly = async (modelName, db, label = Array.isArray(modelName) ? modelName.join(', ') : modelName) => {
602
- const rows = (await db
603
- .select({ ...columns, schemaName: schemas.name })
604
- .from(models)
605
- .leftJoin(modelSchemas, eq(models.id, modelSchemas.modelId))
606
- .leftJoin(schemas, eq(modelSchemas.schemaId, schemas.id))
607
- .where(Array.isArray(modelName) ? inArray(models.name, modelName) : eq(models.name, modelName))
608
- .orderBy(desc(models.id)));
609
- if (rows.length === 0)
610
- return undefined;
611
- const linked = rows.filter((r) => r.schemaName);
612
- const linkedIds = new Set(linked.map((r) => r.id));
613
- if (linkedIds.size > 1) {
614
- throw new AmbiguousModelError(label, linked.map((r) => r.schemaName));
615
- }
616
- const chosen = linked[0] ?? rows[0];
617
- return { id: chosen.id, name: chosen.name, schemaFileId: chosen.schemaFileId };
618
- };
619
- /**
620
- * The model names a model name or EAS model type can refer to: the name itself, else the names
621
- * that match it in the tightest tier (ADR 0008, as Model.findByModelType matches): `new_model` →
622
- * "New model", `sync_storage_post` → "SyncStoragePost", `htmlpage` → "HTMLPage". Each name at its
623
- * nameMatch level (ADR 0009).
624
- */
625
- const modelNamesForNameOrType = async (nameOrType, db) => {
626
- const rows = (await db.selectDistinct({ name: models.name }).from(models));
627
- const names = rows.map((r) => r.name);
628
- if (names.includes(nameOrType))
629
- return [nameOrType];
630
- const levels = await nameMatchLevels(db);
631
- return [...localNamesInBestMatchTier(nameOrType, names, { level: (name) => levels.model(name) })];
632
- };
633
- /**
634
- * resolveModelRecord for a model name **or** an EAS model type (snake_case model name, what sync
635
- * stores in seeds.type): "New model" resolves from `new_model`. Scope and ambiguity behave as in
636
- * resolveModelRecord: modelFileId/modelId first, then the schema, then the name(s) alone, which throws
637
- * AmbiguousModelError when models in several schemas match.
638
- */
639
- const resolveModelRecordByNameOrType = async (nameOrType, scope = {}, db = BaseDb.getAppDb()) => {
640
- if (!db)
641
- return undefined;
642
- if (scope.modelFileId || scope.modelId || scope.propertyFileId) {
643
- const byId = await resolveModelRecord(undefined, scope, db);
644
- if (byId)
645
- return byId;
646
- }
647
- if (!nameOrType)
648
- return undefined;
649
- const names = await modelNamesForNameOrType(nameOrType, db);
650
- if (names.length === 0)
651
- return undefined;
652
- if (names.length === 1) {
653
- return resolveModelRecord(names[0], { ...scope, modelFileId: undefined, modelId: undefined, propertyFileId: undefined }, db);
654
- }
655
- // Several model names map to this type (e.g. "New model" and "NewModel" in different schemas).
656
- if (scope.schemaId || scope.schemaName) {
657
- const rows = (await db
658
- .select(columns)
659
- .from(models)
660
- .innerJoin(modelSchemas, eq(models.id, modelSchemas.modelId))
661
- .innerJoin(schemas, eq(modelSchemas.schemaId, schemas.id))
662
- .where(and(inArray(models.name, names), scope.schemaId ? eq(schemas.id, scope.schemaId) : eq(schemas.name, scope.schemaName))));
663
- if (new Set(rows.map((r) => r.id)).size === 1)
664
- return rows[0];
665
- if (scope.strict && rows.length === 0)
666
- return undefined;
667
- }
668
- return resolveModelRecordByNameOnly(names, db, nameOrType);
669
- };
670
- /**
671
- * The model name that a model name or EAS model type (`seeds.type`, `metadata.model_type`) refers
672
- * to, resolved as {@link resolveModelRecordByNameOrType} resolves it: `blog_post` → "Blog Post"
673
- * through the model's easName at its nameMatch level, never by re-casing the type. Undefined when
674
- * no model matches; throws AmbiguousModelError when models in several schemas do.
675
- */
676
- const modelNameForType = async (nameOrType, scope = {}, db = BaseDb.getAppDb()) => (await resolveModelRecordByNameOrType(nameOrType, scope, db))?.name;
677
- /**
678
- * Work out which model (schemaFileId) an item belongs to from whatever is at hand: an explicit
679
- * modelFileId, the seed's model_file_id, or a metadata row's property_id → properties.model_id.
680
- */
681
- const resolveItemModelFileId = async (hints, db = BaseDb.getAppDb()) => {
682
- if (hints.modelFileId)
683
- return hints.modelFileId;
684
- if (!db)
685
- return undefined;
686
- if (hints.seedLocalId || hints.seedUid) {
687
- const seed = await findSeedRow({ localId: hints.seedLocalId, uid: hints.seedUid }, db);
688
- if (seed?.modelFileId)
689
- return seed.modelFileId;
690
- }
691
- if (hints.propertyId) {
692
- const rows = await db
693
- .select({ modelFileId: models.schemaFileId })
694
- .from(properties)
695
- .innerJoin(models, eq(properties.modelId, models.id))
696
- .where(eq(properties.id, hints.propertyId))
697
- .limit(1);
698
- if (rows[0]?.modelFileId)
699
- return rows[0].modelFileId;
700
- }
701
- return undefined;
702
- };
703
- /** ModelScope for an Item (or ItemProperty) instance: its recorded modelFileId (context or seed row) and schemaName. */
704
- const getItemModelScope = async (item) => {
705
- let context;
706
- try {
707
- context = item.getService?.().getSnapshot().context;
708
- }
709
- catch {
710
- context = undefined;
711
- }
712
- const modelFileId = await resolveItemModelFileId({
713
- modelFileId: context?.modelFileId,
714
- seedLocalId: item.seedLocalId ?? context?.seedLocalId,
715
- seedUid: item.seedUid ?? context?.seedUid,
716
- propertyId: context?.propertyId,
717
- });
718
- return { modelFileId, schemaName: context?.schemaName };
719
- };
720
- /**
721
- * Resolve a ref (e.g. a Relation's target model) by name from the schema(s) its owning model belongs
722
- * to, so "Tag" means the owner schema's Tag rather than any Tag. Falls back to resolveModelRecord.
723
- */
724
- const resolveRefModelRecord = async (refModelName, owner, db = BaseDb.getAppDb()) => {
725
- if (!db || !refModelName)
726
- return undefined;
727
- const ownerRow = owner.modelFileId || owner.modelId ? await resolveModelRecord(undefined, owner, db) : undefined;
728
- const schemaFilter = ownerRow
729
- ? inArray(modelSchemas.schemaId,
730
- // any schema the owner is linked to
731
- db.select({ schemaId: modelSchemas.schemaId }).from(modelSchemas).where(eq(modelSchemas.modelId, ownerRow.id)))
732
- : owner.schemaId
733
- ? eq(modelSchemas.schemaId, owner.schemaId)
734
- : owner.schemaName
735
- ? eq(schemas.name, owner.schemaName)
736
- : undefined;
737
- if (schemaFilter) {
738
- const rows = await db
739
- .select(columns)
740
- .from(models)
741
- .innerJoin(modelSchemas, eq(models.id, modelSchemas.modelId))
742
- .innerJoin(schemas, eq(modelSchemas.schemaId, schemas.id))
743
- .where(and(eq(models.name, refModelName), schemaFilter))
744
- .limit(1);
745
- if (rows.length > 0)
746
- return rows[0];
747
- }
748
- return resolveModelRecord(refModelName, {}, db);
749
- };
750
-
751
- var resolveModelRecord$1 = /*#__PURE__*/Object.freeze({
752
- __proto__: null,
753
- getItemModelScope: getItemModelScope,
754
- getModelSchemaName: getModelSchemaName,
755
- modelNameForType: modelNameForType,
756
- resolveItemModelFileId: resolveItemModelFileId,
757
- resolveModelRecord: resolveModelRecord,
758
- resolveModelRecordByNameOrType: resolveModelRecordByNameOrType,
759
- resolvePropertyModelRecord: resolvePropertyModelRecord,
760
- resolveRefModelRecord: resolveRefModelRecord
761
- });
762
-
763
- /**
764
- * Lowercase + trim an address for persist and compare.
765
- * Does not checksum or validate — test and placeholder addresses must keep working.
766
- */
767
- function normalizeHexAddress(addr) {
768
- return addr.trim().toLowerCase();
769
- }
770
- /**
771
- * Lowercase, drop empties, and dedupe (first-seen order).
772
- */
773
- function normalizeAddressList(addrs) {
774
- if (!addrs?.length)
775
- return [];
776
- const seen = new Set();
777
- const out = [];
778
- for (const addr of addrs) {
779
- if (addr == null || addr === '')
780
- continue;
781
- const normalized = normalizeHexAddress(String(addr));
782
- if (!normalized || seen.has(normalized))
783
- continue;
784
- seen.add(normalized);
785
- out.push(normalized);
786
- }
787
- return out;
788
- }
789
- /**
790
- * Lowercase a publisher stamp. Empty / null → undefined (omit on write).
791
- */
792
- function normalizePublisher(publisher) {
793
- if (publisher == null || publisher === '')
794
- return undefined;
795
- const normalized = normalizeHexAddress(publisher);
796
- return normalized || undefined;
797
- }
798
- /**
799
- * Normalizes AddressConfiguration to { owned, watched }.
800
- * - string[] -> { owned: addresses, watched: [] }
801
- * - { owned, watched? } -> { owned, watched: watched ?? [] }
802
- * Addresses are lowercased and deduped.
803
- */
804
- function normalizeAddressConfig(addresses) {
805
- if (!addresses) {
806
- return { owned: [], watched: [] };
807
- }
808
- if (Array.isArray(addresses)) {
809
- return { owned: normalizeAddressList(addresses), watched: [] };
810
- }
811
- return {
812
- owned: normalizeAddressList(addresses.owned),
813
- watched: normalizeAddressList(addresses.watched),
814
- };
815
- }
816
-
817
- /** Default maximum length for Text properties when not explicitly set. Html holds arbitrary-length content and does not use this limit. */
818
- const DEFAULT_TEXT_MAX_LENGTH = 255;
819
- /**
820
- * TypeBox schema for ValidationRules
821
- */
822
- const TValidationRules = Type.Object({
823
- pattern: Type.Optional(Type.String()),
824
- minLength: Type.Optional(Type.Integer({ minimum: 0 })),
825
- maxLength: Type.Optional(Type.Integer({ minimum: 1 })),
826
- enum: Type.Optional(Type.Array(Type.Any())),
827
- custom: Type.Optional(Type.String()),
828
- }, { additionalProperties: true });
829
-
830
- /**
831
- * Enum for all property data types
832
- */
833
- var ModelPropertyDataTypes;
834
- (function (ModelPropertyDataTypes) {
835
- ModelPropertyDataTypes["Text"] = "Text";
836
- ModelPropertyDataTypes["Number"] = "Number";
837
- ModelPropertyDataTypes["List"] = "List";
838
- ModelPropertyDataTypes["Relation"] = "Relation";
839
- ModelPropertyDataTypes["Image"] = "Image";
840
- ModelPropertyDataTypes["Json"] = "Json";
841
- ModelPropertyDataTypes["File"] = "File";
842
- ModelPropertyDataTypes["Boolean"] = "Boolean";
843
- ModelPropertyDataTypes["Date"] = "Date";
844
- ModelPropertyDataTypes["Html"] = "Html";
845
- })(ModelPropertyDataTypes || (ModelPropertyDataTypes = {}));
846
- /** Known data types in lowercase for normalization */
847
- const DATA_TYPE_LOWER_MAP = {
848
- text: 'Text',
849
- string: 'Text', // JSON Schema convention
850
- number: 'Number',
851
- list: 'List',
852
- relation: 'Relation',
853
- image: 'Image',
854
- json: 'Json',
855
- file: 'File',
856
- boolean: 'Boolean',
857
- date: 'Date',
858
- html: 'Html',
859
- blob: 'Blob',
860
- };
861
- /**
862
- * Normalize dataType to canonical PascalCase form.
863
- * Accepts lowercase (e.g. 'text') or mixed case and returns 'Text'.
864
- * Use when ingesting dataType from JSON schema files.
865
- */
866
- function normalizeDataType(value) {
867
- if (!value || typeof value !== 'string')
868
- return value ?? '';
869
- const lower = value.toLowerCase().trim();
870
- return DATA_TYPE_LOWER_MAP[lower] ?? value.charAt(0).toUpperCase() + value.slice(1).toLowerCase();
871
- }
872
- /**
873
- * Case-insensitive dataType check. Use instead of `dataType === 'List'` so schemas
874
- * (or existing DB rows) with lowercase types like "list" still match.
875
- */
876
- function isDataType(value, expected) {
877
- if (!value)
878
- return false;
879
- return normalizeDataType(value) === expected;
880
- }
881
- /**
882
- * Normalize a property definition to the internal shape ItemProperty reads (`dataType`,
883
- * `refValueType`, `ref`, `storageType`, ...). Accepts schema-file shape (`type`, `model`,
884
- * `items`, `storage`) and any casing. Mirrors the conversion in imports/json createModelFromJson
885
- * for the fallback paths that read schema-file properties directly.
886
- */
887
- function normalizePropertyRecordSchema(def) {
888
- if (!def || typeof def !== 'object')
889
- return def;
890
- const src = def;
891
- const out = { ...src };
892
- const rawType = src.dataType ?? src.type;
893
- if (typeof rawType === 'string' && rawType)
894
- out.dataType = normalizeDataType(rawType);
895
- const ref = src.ref ?? src.refModelName ?? src.items?.model ?? src.model;
896
- if (ref && !out.ref)
897
- out.ref = ref;
898
- if (ref && !out.refModelName)
899
- out.refModelName = ref;
900
- const rawRefValueType = src.refValueType ?? src.items?.type;
901
- if (typeof rawRefValueType === 'string' && rawRefValueType) {
902
- out.refValueType = normalizeDataType(rawRefValueType);
903
- }
904
- else if (out.dataType === ModelPropertyDataTypes.List && ref) {
905
- out.refValueType = ModelPropertyDataTypes.Relation;
906
- }
907
- if (src.storage && typeof src.storage === 'object' && !out.storageType) {
908
- out.storageType = src.storage.type === 'ItemStorage' ? 'ItemStorage' : 'PropertyStorage';
909
- if (src.storage.path !== undefined && out.localStorageDir === undefined)
910
- out.localStorageDir = src.storage.path;
911
- if (src.storage.extension !== undefined && out.filenameSuffix === undefined)
912
- out.filenameSuffix = src.storage.extension;
913
- }
914
- return out;
915
- }
916
- // String literals, not enum members: PropertyDataType must accept plain strings
917
- // such as `{ dataType: 'Text' }`. ModelPropertyDataTypes members still fit.
918
- const TPropertyDataType = Type.Union([
919
- Type.Literal('Text'),
920
- Type.Literal('Number'),
921
- Type.Literal('List'),
922
- Type.Literal('Relation'),
923
- Type.Literal('Image'),
924
- Type.Literal('Json'),
925
- Type.Literal('File'),
926
- Type.Literal('Boolean'),
927
- Type.Literal('Date'),
928
- Type.Literal('Html'),
929
- ]);
930
- const TStorageType = Type.Union([
931
- Type.Literal('ItemStorage'), // Looks for a storageTransactionId property on the item
932
- Type.Literal('PropertyStorage'), // Looks for a storageTransactionId value on the property
933
- ]);
934
- /** How publish handles `data:image/*` URIs inside Html storage. Only applies when `dataType === 'Html'`. */
935
- const THtmlEmbeddedDataUriPolicy = Type.Union([
936
- Type.Literal('materialize'),
937
- Type.Literal('preserve'),
938
- ]);
939
- const TProperty = Type.Object({
940
- id: Type.Optional(Type.String()), // schemaFileId (string) - public ID
941
- _dbId: Type.Optional(Type.Number()), // Database integer ID - internal only
942
- name: Type.Optional(Type.String()),
943
- dataType: TPropertyDataType,
944
- ref: Type.Optional(Type.String()),
945
- // modelId: string (modelFileId) in code; number only for DB foreign keys when resolved
946
- modelId: Type.Optional(Type.Union([Type.Number(), Type.String()])),
947
- modelName: Type.Optional(Type.String()),
948
- refModelId: Type.Optional(Type.Number()),
949
- refModelName: Type.Optional(Type.String()),
950
- refValueType: Type.Optional(TPropertyDataType),
951
- storageType: Type.Optional(TStorageType),
952
- localStorageDir: Type.Optional(Type.String()),
953
- filenameSuffix: Type.Optional(Type.String()),
954
- validation: Type.Optional(TValidationRules),
955
- required: Type.Optional(Type.Boolean()),
956
- /** When set on an Html property: `materialize` uploads embedded images as Image seeds and rewrites src; `preserve` leaves data URIs. */
957
- htmlEmbeddedDataUriPolicy: Type.Optional(THtmlEmbeddedDataUriPolicy),
958
- });
959
- const TPropertyConstructor = Type.Function([
960
- Type.Optional(Type.Union([Type.String(), TStorageType, Type.Undefined()])),
961
- Type.Optional(Type.Union([Type.String(), TPropertyDataType])),
962
- Type.Optional(Type.Union([Type.String(), Type.Boolean()])),
963
- ], TProperty);
964
- const TPropertyDefs = Type.Record(TPropertyDataType, TPropertyConstructor);
965
- function htmlPropertyFactory(opts) {
966
- return {
967
- dataType: ModelPropertyDataTypes.Html,
968
- ...(opts?.htmlEmbeddedDataUriPolicy
969
- ? { htmlEmbeddedDataUriPolicy: opts.htmlEmbeddedDataUriPolicy }
970
- : {}),
971
- };
972
- }
973
- const Property = {
974
- Text: (storageType, localStorageDir, filenameSuffix) => ({
975
- dataType: ModelPropertyDataTypes.Text,
976
- storageType,
977
- localStorageDir,
978
- filenameSuffix,
979
- TObject: Type.String(),
980
- }),
981
- Json: () => ({ dataType: ModelPropertyDataTypes.Json }),
982
- File: () => ({ dataType: ModelPropertyDataTypes.File }),
983
- Number: () => ({ dataType: ModelPropertyDataTypes.Number }),
984
- /**
985
- * Create a List property. refValueType is required; ref is required only for list of relations.
986
- * @param refValueType - Element type: 'Text', 'Number', 'Relation', etc.
987
- * @param ref - Model name when refValueType is 'Relation' (optional for primitives)
988
- * @example List('Text') - list of strings
989
- * @example List('Relation', 'Tag') - list of relations to Tag model
990
- */
991
- List: (refValueType, ref) => ({
992
- dataType: ModelPropertyDataTypes.List,
993
- ref,
994
- refValueType,
995
- }),
996
- Relation: (ref, refValueType, required) => ({
997
- dataType: ModelPropertyDataTypes.Relation,
998
- ref,
999
- refValueType,
1000
- required,
1001
- }),
1002
- Image: () => ({ dataType: ModelPropertyDataTypes.Image }),
1003
- Boolean: () => ({ dataType: ModelPropertyDataTypes.Boolean }),
1004
- Date: () => ({ dataType: ModelPropertyDataTypes.Date }),
1005
- Html: htmlPropertyFactory,
1006
- /** `Html` opts are not representable in `TPropertyConstructor`; keep runtime shape via double assert. */
1007
- };
1008
-
1009
- /** Schema file spellings that normalizePropertyRecordSchema maps to internal fields. */
1010
- const FILE_ALIAS_KEYS = ['type', 'model', 'items', 'storage'];
1011
- /**
1012
- * Fields an instance or a row carries that aren't part of the definition. Keys starting with `_`
1013
- * (`_dbId`, `_propertyFileId`, `_originalValues`, ...) are dropped too.
1014
- */
1015
- const NON_DEFINITION_KEYS = new Set([
1016
- ...FILE_ALIAS_KEYS,
1017
- 'name',
1018
- 'modelId',
1019
- 'modelName',
1020
- 'refModelId',
1021
- 'refModelName',
1022
- 'schemaName',
1023
- 'isEdited',
1024
- 'TObject',
1025
- ]);
1026
- const parseDefinition = (json) => {
1027
- if (!json)
1028
- return {};
1029
- try {
1030
- const parsed = JSON.parse(json);
1031
- return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
1032
- }
1033
- catch {
1034
- return {};
1035
- }
1036
- };
1037
- /** A definition in any accepted shape, reduced to what the column stores. */
1038
- const toStoredDefinition = (def) => {
1039
- const normalized = normalizePropertyRecordSchema(def ?? {});
1040
- const hasRef = normalized.dataType === ModelPropertyDataTypes.Relation || normalized.dataType === ModelPropertyDataTypes.List;
1041
- const out = {};
1042
- for (const [key, value] of Object.entries(normalized)) {
1043
- if (value === undefined || typeof value === 'function')
1044
- continue;
1045
- if (key.startsWith('_') || NON_DEFINITION_KEYS.has(key))
1046
- continue;
1047
- if (key === 'ref' && !hasRef)
1048
- continue;
1049
- out[key] = value;
1050
- }
1051
- return out;
1052
- };
1053
- /**
1054
- * The `definition` column value for a definition in any accepted shape (schema file property,
1055
- * Model.create property, ModelProperty context). With `previous` (the row's current value), fields
1056
- * the new definition doesn't have are kept: a write that only knows the columns (a creation
1057
- * payload, a row-built instance) must not drop the validation a schema file or code stored.
1058
- */
1059
- function propertyDefinitionColumn(def, previous) {
1060
- const stored = toStoredDefinition(def);
1061
- return JSON.stringify(previous ? { ...toStoredDefinition(parseDefinition(previous)), ...stored } : stored);
1062
- }
1063
- /**
1064
- * A row as a {@link PropertyDefinition}. `refModelName` is the name of the row's `ref_model_id`
1065
- * model; without one, a Relation or List keeps the definition's `ref` (a target not stored yet).
1066
- */
1067
- function propertyDefinitionFromRow(row, refModelName) {
1068
- const stored = toStoredDefinition(parseDefinition(row.definition));
1069
- const dataType = normalizeDataType(row.dataType);
1070
- const hasRef = dataType === ModelPropertyDataTypes.Relation || dataType === ModelPropertyDataTypes.List;
1071
- const ref = refModelName ?? (hasRef ? stored.ref : undefined);
1072
- const def = {
1073
- ...stored,
1074
- id: row.schemaFileId ?? undefined,
1075
- _dbId: row.id,
1076
- name: row.name,
1077
- modelId: row.modelId,
1078
- dataType,
1079
- ref,
1080
- refModelName: ref,
1081
- refModelId: row.refModelId ?? undefined,
1082
- refValueType: row.refValueType ? normalizeDataType(row.refValueType) : undefined,
1083
- storageType: row.storageType ?? undefined,
1084
- localStorageDir: row.localStorageDir ?? undefined,
1085
- filenameSuffix: row.filenameSuffix ?? undefined,
1086
- required: row.required ?? stored.required ?? undefined,
1087
- };
1088
- for (const key of Object.keys(def))
1089
- if (def[key] === undefined)
1090
- delete def[key];
1091
- return def;
1092
- }
1093
- const refModels = alias(models, 'ref_model');
1094
- const selectDefinitionRows = (db) => db
1095
- .select({ property: properties, refModelName: refModels.name })
1096
- .from(properties)
1097
- .leftJoin(refModels, eq(refModels.id, properties.refModelId));
1098
- /**
1099
- * The complete definition of one property, from its row: by row id (`metadata.property_id`), or by
1100
- * model row id and property name. Undefined when there's no such row.
1101
- */
1102
- async function getPropertyDefinition(target, db = BaseDb.getAppDb()) {
1103
- if (!db)
1104
- throw new Error('Database not available');
1105
- const where = typeof target === 'number'
1106
- ? eq(properties.id, target)
1107
- : and(eq(properties.modelId, target.modelId), eq(properties.name, target.name));
1108
- const [row] = await selectDefinitionRows(db).where(where).limit(1);
1109
- return row ? propertyDefinitionFromRow(row.property, row.refModelName) : undefined;
1110
- }
1111
- /** {@link getPropertyDefinition} for every property of one model, by property name, in one read. */
1112
- async function getPropertyDefinitions(modelId, db = BaseDb.getAppDb()) {
1113
- if (!db)
1114
- throw new Error('Database not available');
1115
- const rows = await selectDefinitionRows(db).where(eq(properties.modelId, modelId));
1116
- return new Map(rows.map((row) => [
1117
- row.property.name,
1118
- propertyDefinitionFromRow(row.property, row.refModelName),
1119
- ]));
1120
- }
1121
- /** {@link getPropertyDefinition} for each of these property file ids that has a row, by file id. */
1122
- async function getPropertyDefinitionsByFileId(propertyFileIds, db = BaseDb.getAppDb()) {
1123
- if (!db)
1124
- throw new Error('Database not available');
1125
- if (propertyFileIds.length === 0)
1126
- return new Map();
1127
- const rows = await selectDefinitionRows(db).where(inArray(properties.schemaFileId, propertyFileIds));
1128
- return new Map(rows.map((row) => [
1129
- row.property.schemaFileId,
1130
- propertyDefinitionFromRow(row.property, row.refModelName),
1131
- ]));
1132
- }
1133
- /** Fields only a definition holds (no column), which code-defined properties can set. */
1134
- const DEFINITION_ONLY_FIELDS = ['validation', 'htmlEmbeddedDataUriPolicy', 'description'];
1135
- /** The {@link DEFINITION_ONLY_FIELDS} a definition sets. */
1136
- const definitionOnlyFields = (def) => Object.fromEntries(DEFINITION_ONLY_FIELDS.filter((key) => def?.[key] !== undefined).map((key) => [key, def[key]]));
1137
- /**
1138
- * Stores code-defined fields ({@link definitionOnlyFields}) in the row's definition, when the row
1139
- * exists and its definition differs. For properties whose row was written before the code that
1140
- * defines them ran (a schema file's row, an earlier run), so their rules persist at create time.
1141
- */
1142
- async function persistDefinitionFields(where, fields, db = BaseDb.getAppDb()) {
1143
- if (!db || Object.keys(fields).length === 0)
1144
- return;
1145
- const condition = 'id' in where ? eq(properties.id, where.id) : eq(properties.schemaFileId, where.schemaFileId);
1146
- const [row] = await db.select({ id: properties.id, definition: properties.definition }).from(properties).where(condition).limit(1);
1147
- if (!row)
1148
- return;
1149
- const next = propertyDefinitionColumn(fields, row.definition);
1150
- if (row.definition && JSON.stringify(toStoredDefinition(parseDefinition(row.definition))) === next)
1151
- return;
1152
- await db.update(properties).set({ definition: next }).where(eq(properties.id, row.id));
1153
- }
1154
-
1155
- var propertyDefinition = /*#__PURE__*/Object.freeze({
1156
- __proto__: null,
1157
- DEFINITION_ONLY_FIELDS: DEFINITION_ONLY_FIELDS,
1158
- definitionOnlyFields: definitionOnlyFields,
1159
- getPropertyDefinition: getPropertyDefinition,
1160
- getPropertyDefinitions: getPropertyDefinitions,
1161
- getPropertyDefinitionsByFileId: getPropertyDefinitionsByFileId,
1162
- persistDefinitionFields: persistDefinitionFields,
1163
- propertyDefinitionColumn: propertyDefinitionColumn,
1164
- propertyDefinitionFromRow: propertyDefinitionFromRow
1165
- });
1166
-
1167
- /**
1168
- * The writers of definition rows: `schemas`, `models`, `properties` and `model_schemas` (SDK audit
1169
- * T9). One upsert per table. Every function takes the database to write, normally a transaction
1170
- * from {@link inDefinitionsTransaction}, and reads only through it: in the browser a query on
1171
- * `BaseDb.getAppDb()` waits for an open transaction to end.
1172
- *
1173
- * The lookups here decide which row a definition is (a schema by file id, then by name; a model by
1174
- * file id, then an adoptable row of that name; a property by file id, then by name in its model).
1175
- * The unique keys (`schema_file_id` on all three tables, `properties(name, model_id)`,
1176
- * `model_schemas(model_id, schema_id)`) are enforced by the database, and inserts resolve a
1177
- * conflict with ON CONFLICT instead of a second lookup.
1178
- */
1179
- const logger$7 = debug('seedSdk:db:write:definitions');
1180
- /**
1181
- * Runs one user action on definitions (an apply, a save, a rename) in one transaction. `fn` must
1182
- * write through `tx` only and have no other side effects: it is retried whole when another tab
1183
- * holds the database.
1184
- */
1185
- const inDefinitionsTransaction = (fn) => BaseDb.transaction(fn);
1186
- // ---------------------------------------------------------------------------------------------
1187
- // model_schemas
1188
- /** Links a model to a schema. Returns true when this call inserted the link. */
1189
- async function linkModelToSchemaRow(db, modelId, schemaId) {
1190
- const inserted = await db
1191
- .insert(modelSchemas)
1192
- .values({ modelId, schemaId })
1193
- .onConflictDoNothing()
1194
- .returning({ id: modelSchemas.id });
1195
- return inserted.length > 0;
1196
- }
1197
- /**
1198
- * Writes a schema row and returns it with `inserted` set when it is new.
1199
- *
1200
- * The row is the one with `schemaFileId`, else one of that name: a draft write takes a draft of
1201
- * that name; a published write takes a published row, else a draft (which it publishes). A
1202
- * published row keeps its schemaFileId against another id of the same version, and keeps its newer
1203
- * version against an older version's file. With `rowId` that row is written, found by the caller
1204
- * (a rename finds it by the old name), and takes `schemaFileId`.
1205
- */
1206
- async function upsertSchemaRow(db, input, options = {}) {
1207
- // The stored schema file may change its nameMatch levels (ADR 0009).
1208
- clearNameMatchLevels();
1209
- const { schemaFileId, schemaData, isDraft, isEdited, contentHash } = input;
1210
- const update = async (existing, updates) => {
1211
- if (input.name && existing.name !== input.name)
1212
- updates.name = input.name;
1213
- if (schemaData !== undefined)
1214
- updates.schemaData = schemaData;
1215
- if (contentHash !== undefined && existing.contentHash !== contentHash)
1216
- updates.contentHash = contentHash;
1217
- if (input.version !== undefined && existing.version !== input.version)
1218
- updates.version = input.version;
1219
- if (isDraft !== undefined && existing.isDraft !== isDraft)
1220
- updates.isDraft = isDraft;
1221
- if (isEdited !== undefined && existing.isEdited !== isEdited)
1222
- updates.isEdited = isEdited;
1223
- if (input.updatedAt && existing.updatedAt !== input.updatedAt)
1224
- updates.updatedAt = input.updatedAt;
1225
- if (Object.keys(updates).length > 0) {
1226
- await db.update(schemas).set(updates).where(eq(schemas.id, existing.id));
1227
- }
1228
- return { ...existing, ...updates, inserted: false };
1229
- };
1230
- if (options.rowId !== undefined) {
1231
- const [row] = await db.select().from(schemas).where(eq(schemas.id, options.rowId)).limit(1);
1232
- if (!row)
1233
- throw new Error(`Schema row ${options.rowId} not found`);
1234
- return update(row, schemaFileId && row.schemaFileId !== schemaFileId ? { schemaFileId } : {});
1235
- }
1236
- if (schemaFileId) {
1237
- const [byFileId] = await db.select().from(schemas).where(eq(schemas.schemaFileId, schemaFileId)).limit(1);
1238
- if (byFileId)
1239
- return update(byFileId, {});
1240
- }
1241
- if (isDraft === true) {
1242
- const [byName] = await db.select().from(schemas).where(eq(schemas.name, input.name)).limit(1);
1243
- if (byName?.isDraft === true) {
1244
- const updates = { updatedAt: input.updatedAt || Date.now() };
1245
- if (schemaFileId && byName.schemaFileId !== schemaFileId)
1246
- updates.schemaFileId = schemaFileId;
1247
- return update(byName, updates);
1248
- }
1249
- }
1250
- // By name: a published write prefers a published row, then takes a draft of that name.
1251
- let [existing] = await db
1252
- .select()
1253
- .from(schemas)
1254
- .where(isDraft === false ? and(eq(schemas.name, input.name), eq(schemas.isDraft, false)) : eq(schemas.name, input.name))
1255
- .limit(1);
1256
- if (!existing && isDraft === false) {
1257
- [existing] = await db
1258
- .select()
1259
- .from(schemas)
1260
- .where(and(eq(schemas.name, input.name), eq(schemas.isDraft, true)))
1261
- .limit(1);
1262
- }
1263
- if (existing) {
1264
- // A complete schema file can bring a version under its own id (imported files may not keep it).
1265
- const isOtherVersionFile = !!schemaFileId &&
1266
- !!existing.schemaFileId &&
1267
- existing.schemaFileId !== schemaFileId &&
1268
- existing.isDraft !== true &&
1269
- input.version !== undefined &&
1270
- input.version !== existing.version;
1271
- if (isOtherVersionFile && input.version < existing.version) {
1272
- // An older version's file: rolling the row back to it would leave the newer version's id
1273
- // pointing at the older version's data.
1274
- logger$7(`Keeping schema "${input.name}" at v${existing.version}; not replacing it with older v${input.version}`);
1275
- return { ...existing, inserted: false };
1276
- }
1277
- const updates = {};
1278
- // A published row keeps its id unless this is a newer version's file; a draft or id-less row
1279
- // takes the new one.
1280
- if (schemaFileId &&
1281
- existing.schemaFileId !== schemaFileId &&
1282
- (existing.isDraft === true || !existing.schemaFileId || isOtherVersionFile)) {
1283
- updates.schemaFileId = schemaFileId;
1284
- }
1285
- return update(existing, updates);
1286
- }
1287
- const [row] = await db
1288
- .insert(schemas)
1289
- .values({
1290
- name: input.name,
1291
- version: input.version,
1292
- schemaFileId: schemaFileId || null,
1293
- schemaData: schemaData || null,
1294
- contentHash: contentHash ?? null,
1295
- isDraft: isDraft ?? false,
1296
- isEdited: isEdited ?? false,
1297
- createdAt: input.createdAt,
1298
- updatedAt: input.updatedAt,
1299
- })
1300
- .returning();
1301
- return { ...row, inserted: true };
1302
- }
1303
- // ---------------------------------------------------------------------------------------------
1304
- // models
1305
- /**
1306
- * Writes the models row with `schemaFileId`, or inserts a name-only row without one. An existing
1307
- * row takes `name`; `isEdited` is set on insert only.
1308
- */
1309
- async function upsertModelRow(db, input) {
1310
- const values = { name: input.name, schemaFileId: input.schemaFileId || null, isEdited: input.isEdited ?? false };
1311
- if (!values.schemaFileId) {
1312
- const [row] = await db.insert(models).values(values).returning();
1313
- return row;
1314
- }
1315
- const [row] = await db
1316
- .insert(models)
1317
- .values(values)
1318
- .onConflictDoUpdate({ target: models.schemaFileId, set: { name: input.name } })
1319
- .returning();
1320
- return row;
1321
- }
1322
- /** Name-only models rows for the names that have none yet. Returns every row of those names. */
1323
- async function insertModelStubs(db, modelNames) {
1324
- const columns = { id: models.id, name: models.name, schemaFileId: models.schemaFileId };
1325
- const rows = modelNames.length
1326
- ? await db.select(columns).from(models).where(inArray(models.name, modelNames))
1327
- : [];
1328
- const missing = [...new Set(modelNames)].filter((name) => !rows.some((row) => row.name === name));
1329
- if (missing.length > 0) {
1330
- rows.push(...(await db.insert(models).values(missing.map((name) => ({ name }))).returning(columns)));
1331
- }
1332
- return rows;
1333
- }
1334
- const isLinkedTo = async (db, modelId, schemaId) => {
1335
- const links = await db
1336
- .select({ schemaId: modelSchemas.schemaId })
1337
- .from(modelSchemas)
1338
- .where(eq(modelSchemas.modelId, modelId));
1339
- return {
1340
- any: links.length > 0,
1341
- other: links.some((link) => link.schemaId !== schemaId),
1342
- };
1343
- };
1344
- /**
1345
- * Finds or creates the models row for `modelName` in a schema, by `modelFileId` (preferred) or
1346
- * name. Model names are only unique per schema, so a same-name row is reused only when no other
1347
- * schema claims it: a null-schemaFileId stub (from ref resolution or init), or for a ref without a
1348
- * file id, this schema's row. A row with a file id that no schema links is a deleted schema's
1349
- * leftover and is not adopted. An adopted row takes `modelFileId`.
1350
- */
1351
- async function findOrCreateModelRow(db, modelName, modelFileId, schemaId) {
1352
- if (modelFileId) {
1353
- const [byFileId] = await db.select().from(models).where(eq(models.schemaFileId, modelFileId)).limit(1);
1354
- if (byFileId) {
1355
- if (byFileId.name === modelName)
1356
- return byFileId;
1357
- await db.update(models).set({ name: modelName }).where(eq(models.id, byFileId.id));
1358
- return { ...byFileId, name: modelName };
1359
- }
1360
- }
1361
- const byName = await db.select().from(models).where(eq(models.name, modelName));
1362
- let chosen;
1363
- if (byName.length > 0) {
1364
- if (!modelFileId && schemaId && byName.length > 1) {
1365
- for (const candidate of byName) {
1366
- const links = await isLinkedTo(db, candidate.id, schemaId);
1367
- if (links.any && !links.other) {
1368
- chosen = candidate;
1369
- break;
1370
- }
1371
- }
1372
- }
1373
- chosen ?? (chosen = byName.find((row) => !row.schemaFileId));
1374
- if (!chosen && modelFileId) {
1375
- // Every same-name row has another file id: adopt only one no other schema claims.
1376
- for (const candidate of byName) {
1377
- const links = await isLinkedTo(db, candidate.id, schemaId);
1378
- if (candidate.schemaFileId && !links.any)
1379
- continue;
1380
- if (!links.other) {
1381
- chosen = candidate;
1382
- break;
1383
- }
1384
- }
1385
- }
1386
- else if (!chosen) {
1387
- chosen = (byName.length === 1 ? byName[0] : undefined) || byName.find((row) => !!row.schemaFileId) || byName[0];
1388
- }
1389
- }
1390
- if (chosen && modelFileId && chosen.schemaFileId !== modelFileId) {
1391
- // Adopt a stub or an unclaimed same-name row by giving it this file id.
1392
- await db.update(models).set({ schemaFileId: modelFileId, isEdited: false }).where(eq(models.id, chosen.id));
1393
- logger$7(`Adopted existing model "${modelName}" (id: ${chosen.id}) with schemaFileId "${modelFileId}"`);
1394
- return { ...chosen, schemaFileId: modelFileId, isEdited: false };
1395
- }
1396
- if (chosen)
1397
- return chosen;
1398
- return upsertModelRow(db, { name: modelName, schemaFileId: modelFileId });
1399
- }
1400
- /**
1401
- * Finds or creates the models row a Relation property on `ownerModelId` points to: the ref model
1402
- * in the owner's schema(s) first, since model names are only unique per schema.
1403
- */
1404
- async function findOrCreateRefModelRow(db, refModelName, ownerModelId) {
1405
- try {
1406
- const ref = await resolveRefModelRecord(refModelName, { modelId: ownerModelId }, db);
1407
- if (ref)
1408
- return ref;
1409
- }
1410
- catch (error) {
1411
- // Not in the owner's schema and defined by several others: pick/create like an import does
1412
- if (!(error instanceof AmbiguousModelError))
1413
- throw error;
1414
- }
1415
- const [ownerLink] = await db
1416
- .select({ schemaId: modelSchemas.schemaId })
1417
- .from(modelSchemas)
1418
- .where(eq(modelSchemas.modelId, ownerModelId))
1419
- .limit(1);
1420
- return findOrCreateModelRow(db, refModelName, undefined, ownerLink?.schemaId ?? undefined);
1421
- }
1422
- /**
1423
- * Renames a model. With `schemaNameOrId` only that schema's model is renamed; without it the name
1424
- * alone is used, which throws AmbiguousModelError when several schemas define it. When the new
1425
- * name already exists in the scope that row is returned unchanged. Unscoped, a missing model is
1426
- * inserted under the new name.
1427
- */
1428
- async function renameModelRow(db, oldName, newName, schemaNameOrId) {
1429
- const scope = schemaNameOrId === undefined
1430
- ? {}
1431
- : typeof schemaNameOrId === 'number'
1432
- ? { schemaId: schemaNameOrId, strict: true }
1433
- : { schemaName: schemaNameOrId, strict: true };
1434
- const model = await resolveModelRecord(oldName, scope, db);
1435
- if (!model) {
1436
- if (schemaNameOrId !== undefined)
1437
- throw new Error(`Model "${oldName}" not found in schema "${schemaNameOrId}"`);
1438
- return upsertModelRow(db, { name: newName });
1439
- }
1440
- const existingWithNewName = await resolveModelRecord(newName, scope, db);
1441
- if (existingWithNewName)
1442
- return existingWithNewName;
1443
- await db.update(models).set({ name: newName }).where(eq(models.id, model.id));
1444
- return { ...model, name: newName };
1445
- }
1446
- const propertyRowFields = [
1447
- 'name',
1448
- 'modelId',
1449
- 'dataType',
1450
- 'schemaFileId',
1451
- 'refModelId',
1452
- 'refValueType',
1453
- 'required',
1454
- 'isEdited',
1455
- 'storageType',
1456
- 'localStorageDir',
1457
- 'filenameSuffix',
1458
- 'definition',
1459
- ];
1460
- /**
1461
- * Writes the properties row for a property definition. The row is the one with `schemaFileId`,
1462
- * else the one named `previousName` or `name` in the model. A row with that schema_file_id on
1463
- * another model is left alone. The rules for fields the input leaves out are on
1464
- * {@link PropertyRowInput}; an unchanged row is not written.
1465
- */
1466
- async function upsertPropertyRow(db, input, options = {}) {
1467
- let existing;
1468
- if (input.schemaFileId) {
1469
- [existing] = await db.select().from(properties).where(eq(properties.schemaFileId, input.schemaFileId)).limit(1);
1470
- if (existing && existing.modelId !== input.modelId) {
1471
- logger$7(`Property ${input.name} (${input.schemaFileId}) belongs to model row ${existing.modelId}, not ${input.modelId}; left as is`);
1472
- return { row: existing, changed: false };
1473
- }
1474
- }
1475
- for (const name of [input.previousName, input.name]) {
1476
- if (existing || !name)
1477
- continue;
1478
- [existing] = await db
1479
- .select()
1480
- .from(properties)
1481
- .where(and(eq(properties.name, name), eq(properties.modelId, input.modelId)))
1482
- .limit(1);
1483
- }
1484
- if (existing && options.keepEditedRow && existing.isEdited === true) {
1485
- logger$7(`Keeping locally edited property row ${existing.name} (id ${existing.id})`);
1486
- return { row: existing, changed: false };
1487
- }
1488
- let refModelId = input.refModelId ?? null;
1489
- if (input.refModelName && input.refModelId == null) {
1490
- refModelId = (await findOrCreateRefModelRow(db, input.refModelName, input.modelId)).id ?? null;
1491
- }
1492
- const values = {
1493
- name: input.name,
1494
- modelId: input.modelId,
1495
- dataType: input.dataType ? normalizeDataType(input.dataType) : (existing?.dataType ?? ''),
1496
- schemaFileId: (options.keepStoredSchemaFileId && existing?.schemaFileId) || input.schemaFileId || existing?.schemaFileId || null,
1497
- refModelId,
1498
- refValueType: input.refValueType ? normalizeDataType(input.refValueType) : null,
1499
- required: input.required ?? existing?.required ?? false,
1500
- isEdited: input.isEdited ?? existing?.isEdited ?? false,
1501
- storageType: input.storage ? (input.storage.storageType ?? null) : (existing?.storageType ?? null),
1502
- localStorageDir: input.storage ? (input.storage.localStorageDir ?? null) : (existing?.localStorageDir ?? null),
1503
- filenameSuffix: input.storage ? (input.storage.filenameSuffix ?? null) : (existing?.filenameSuffix ?? null),
1504
- definition: input.definition
1505
- ? propertyDefinitionColumn(input.definition, input.replaceDefinition ? null : existing?.definition)
1506
- : (existing?.definition ?? null),
1507
- };
1508
- if (existing) {
1509
- const unchanged = propertyRowFields.every((field) => (existing[field] ?? null) === (values[field] ?? null));
1510
- if (unchanged)
1511
- return { row: existing, changed: false };
1512
- await db.update(properties).set(values).where(eq(properties.id, existing.id));
1513
- return { row: { ...existing, ...values }, changed: true };
1514
- }
1515
- const { name, modelId, ...rest } = values;
1516
- const [row] = await db
1517
- .insert(properties)
1518
- .values(values)
1519
- .onConflictDoUpdate(values.schemaFileId
1520
- ? { target: properties.schemaFileId, set: { name, ...rest } }
1521
- : { target: [properties.name, properties.modelId], set: rest })
1522
- .returning();
1523
- return { row, changed: true };
1524
- }
1525
-
1526
- var definitions = /*#__PURE__*/Object.freeze({
1527
- __proto__: null,
1528
- findOrCreateModelRow: findOrCreateModelRow,
1529
- findOrCreateRefModelRow: findOrCreateRefModelRow,
1530
- inDefinitionsTransaction: inDefinitionsTransaction,
1531
- insertModelStubs: insertModelStubs,
1532
- linkModelToSchemaRow: linkModelToSchemaRow,
1533
- renameModelRow: renameModelRow,
1534
- upsertModelRow: upsertModelRow,
1535
- upsertPropertyRow: upsertPropertyRow,
1536
- upsertSchemaRow: upsertSchemaRow
1537
- });
1538
-
1539
- const logger$6 = debug('seedSdk:helpers:db');
1540
- /**
1541
- * Posts one message and closes the channel. A message already posted is still delivered after its
1542
- * sender closes, and an open BroadcastChannel keeps a Node process alive (each was a MessagePort
1543
- * handle that stopped scripts exiting).
1544
- */
1545
- const postBroadcast = (channelName, message) => {
1546
- const channel = new BroadcastChannel(channelName);
1547
- try {
1548
- channel.postMessage(message);
1549
- }
1550
- finally {
1551
- channel.close();
1552
- }
1553
- };
1554
- /**
1555
- * The `properties` storage columns for a property definition, in either the internal shape
1556
- * (`storageType`, `localStorageDir`, `filenameSuffix`) or the schema-file shape
1557
- * (`storage: { type, path, extension }`). All null when the definition has no storage config.
1558
- */
1559
- function propertyStorageColumns(def) {
1560
- const normalized = normalizePropertyRecordSchema((def ?? {}));
1561
- return {
1562
- storageType: normalized.storageType ?? null,
1563
- localStorageDir: normalized.localStorageDir ?? null,
1564
- filenameSuffix: normalized.filenameSuffix ?? null,
1565
- };
1566
- }
1567
- /** True when the definition carries any storage setting (so writing its columns can't wipe them by omission). */
1568
- const hasStorageSettings = (columns) => columns.storageType != null || columns.localStorageDir != null || columns.filenameSuffix != null;
1569
- /**
1570
- * A model's ItemStorage properties as stored in `properties` (storage_type 'ItemStorage'), with
1571
- * their storage path and extension. Reads the table, not ModelProperty instances, so it doesn't
1572
- * depend on whether (or from where) the model's property instances have loaded.
1573
- */
1574
- async function getItemStoragePropertiesForModel(modelNameOrType, scope = {}) {
1575
- const db = BaseDb.getAppDb();
1576
- if (!db || !modelNameOrType)
1577
- return [];
1578
- const modelRecord = await resolveModelRecordByNameOrType(modelNameOrType, scope, db);
1579
- if (!modelRecord)
1580
- return [];
1581
- return db
1582
- .select({
1583
- id: properties.id,
1584
- name: properties.name,
1585
- localStorageDir: properties.localStorageDir,
1586
- filenameSuffix: properties.filenameSuffix,
1587
- })
1588
- .from(properties)
1589
- .where(and(eq(properties.modelId, modelRecord.id), eq(properties.storageType, 'ItemStorage')));
1590
- }
1591
- /**
1592
- * Moves a renamed property's pending outbox edits (ADR 0019) to its new name, so they apply as the
1593
- * property's. Its metadata rows need no move: readers match them by property_id (decision D4).
1594
- * Returns the number of ops renamed.
1595
- */
1596
- async function renamePendingWritesForProperty(modelName, oldPropertyName, newPropertyName) {
1597
- const db = BaseDb.getAppDb();
1598
- if (!db)
1599
- return 0;
1600
- const renamed = await db
1601
- .update(pendingWrites)
1602
- .set({ propertyName: newPropertyName })
1603
- .where(and(eq(pendingWrites.propertyName, oldPropertyName), eq(pendingWrites.status, 'pending'), inArray(pendingWrites.seedLocalId, db.select({ localId: seeds.localId }).from(seeds).where(eq(seeds.type, easName(modelName))))))
1604
- .returning({ opId: pendingWrites.opId });
1605
- return renamed.length;
1606
- }
1607
- /** Tells React's useSchemas a schemas row was inserted (its live query may not re-run). */
1608
- const broadcastSchemasInvalidate = () => {
1609
- if (typeof BroadcastChannel === 'undefined')
1610
- return;
1611
- try {
1612
- postBroadcast('seed-schemas-invalidate', {});
1613
- }
1614
- catch (_) { }
1615
- };
1616
- /** Tells React's useModels a model was linked to a schema (its live query may not re-run). */
1617
- const broadcastModelsInvalidate = (schema) => {
1618
- if (typeof BroadcastChannel === 'undefined')
1619
- return;
1620
- try {
1621
- postBroadcast('seed-models-invalidate', {
1622
- schemaName: schema.name ?? undefined,
1623
- schemaFileId: schema.schemaFileId ?? undefined,
1624
- });
1625
- }
1626
- catch (_) { }
1627
- };
1628
- /**
1629
- * Writes a schema row ({@link upsertSchemaRow} has the matching rules) and returns it.
1630
- * @param schemaFileId - Optional schemaFileId from JSON file for change tracking
1631
- * @param schemaData - Optional full schema content as JSON string (SchemaFileFormat)
1632
- * @param isDraft - Optional flag indicating if schema is in draft state (default: false)
1633
- * @param isEdited - Optional flag indicating if schema has been edited locally (default: false)
1634
- * @param options.db - Write on this transaction; the caller then tells React after it commits.
1635
- * Without it the write runs in its own transaction.
1636
- * @param options.contentHash - The applied file's hash (applySchemaFile); written with schemaData
1637
- */
1638
- const addSchemaToDb = async (schema, schemaFileId, schemaData, isDraft, isEdited, options = {}) => {
1639
- if (!options.db && !BaseDb.getAppDb()) {
1640
- throw new Error('Database not found');
1641
- }
1642
- const input = {
1643
- name: schema.name,
1644
- version: schema.version,
1645
- createdAt: schema.createdAt,
1646
- updatedAt: schema.updatedAt,
1647
- schemaFileId,
1648
- schemaData,
1649
- isDraft,
1650
- isEdited,
1651
- contentHash: options.contentHash,
1652
- };
1653
- if (options.db) {
1654
- const { inserted: _inserted, ...row } = await upsertSchemaRow(options.db, input);
1655
- return row;
1656
- }
1657
- const { inserted, ...row } = await inDefinitionsTransaction((tx) => upsertSchemaRow(tx, input));
1658
- if (inserted)
1659
- broadcastSchemasInvalidate();
1660
- return row;
1661
- };
1662
- /**
1663
- * Renames a model ({@link renameModelRow}), in its own transaction unless `db` is given.
1664
- * @param schemaNameOrId - Schema name or id to scope the rename (required when multiple schemas have models with the same name)
1665
- */
1666
- const renameModelInDb = async (oldName, newName, schemaNameOrId, db) => {
1667
- if (db)
1668
- return renameModelRow(db, oldName, newName, schemaNameOrId);
1669
- if (!BaseDb.getAppDb())
1670
- throw new Error('Database not found');
1671
- return inDefinitionsTransaction((tx) => renameModelRow(tx, oldName, newName, schemaNameOrId));
1672
- };
1673
- /**
1674
- * Writes a schema file's models, their property rows and their model_schemas links.
1675
- * @param models - Models to write besides those in schemaFileData.propertyDefinitions
1676
- * @param schema - Optional schema to link models to (a row, or written first when it has no id)
1677
- * @param modelRenames - Optional map of old model names to new model names
1678
- * @param schemaFileData - The file's ids and property definitions: { schemaFileId, modelFileIds: Map<modelName, id>, propertyFileIds: Map<modelName, Map<propertyName, id>>, propertyDefinitions }
1679
- * @param options.db - Write on this transaction; the caller refreshes cached ModelProperty
1680
- * instances with the returned ids once it has committed. Without it the write runs in its own
1681
- * transaction and refreshes them itself.
1682
- * @returns The schemaFileIds of the property rows this call changed
1683
- */
1684
- const addModelsToDb = async (models, schema, modelRenames, schemaFileData, options = {}) => {
1685
- if (!options.db) {
1686
- if (!BaseDb.getAppDb())
1687
- throw new Error('Database not found');
1688
- const changed = await inDefinitionsTransaction((tx) => addModelsToDb(models, schema, modelRenames, schemaFileData, { db: tx }));
1689
- // A cached instance is otherwise never re-read from its row (createById returns it as is)
1690
- await refreshCachedProperties(changed);
1691
- return changed;
1692
- }
1693
- const db = options.db;
1694
- // Model rows and their schema links decide which schema file's nameMatch applies (ADR 0009).
1695
- clearNameMatchLevels();
1696
- // The schema may already be a row (with id)
1697
- let schemaRecord;
1698
- if (schema) {
1699
- schemaRecord =
1700
- schema.id !== null && schema.id !== undefined
1701
- ? schema
1702
- : await addSchemaToDb(schema, schemaFileData?.schemaFileId, undefined, undefined, undefined, { db });
1703
- }
1704
- if (modelRenames && schemaRecord) {
1705
- const schemaNameOrId = schemaRecord.name ?? schemaRecord.id;
1706
- for (const [oldName, newName] of modelRenames.entries()) {
1707
- await renameModelRow(db, oldName, newName, schemaNameOrId);
1708
- }
1709
- }
1710
- const modelRecords = new Map();
1711
- const modelProcessingErrors = [];
1712
- // Property rows this call changed, so cached ModelProperty instances can be given the new values
1713
- const updatedPropertyFileIds = [];
1714
- const { generateId } = await Promise.resolve().then(function () { return generateId$1; });
1715
- const modelNames = [...new Set([...Object.keys(models), ...(schemaFileData?.propertyDefinitions?.keys() ?? [])])];
1716
- for (const modelName of modelNames) {
1717
- try {
1718
- const modelFileId = schemaFileData?.modelFileIds?.get(modelName);
1719
- const modelRecord = modelRecords.get(modelName) ?? (await findOrCreateModelRow(db, modelName, modelFileId, schemaRecord?.id));
1720
- modelRecords.set(modelName, modelRecord);
1721
- const definitions = schemaFileData?.propertyDefinitions?.get(modelName) ?? {};
1722
- for (const [propertyName, propertyValues] of Object.entries(definitions)) {
1723
- if (!propertyValues) {
1724
- throw new Error(`Property values not found for ${propertyName}`);
1725
- }
1726
- // IDs are assigned before this is called; a generated one doesn't replace a stored id.
1727
- const propertyFileId = schemaFileData?.propertyFileIds?.get(modelName)?.get(propertyName);
1728
- let refModelId = null;
1729
- const refModelName = propertyValues.ref || propertyValues.refModelName;
1730
- if (refModelName) {
1731
- let refModel = modelRecords.get(refModelName);
1732
- if (!refModel) {
1733
- const refModelFileId = schemaFileData?.modelFileIds?.get(refModelName);
1734
- refModel = await findOrCreateModelRow(db, refModelName, refModelFileId, schemaRecord?.id);
1735
- modelRecords.set(refModelName, refModel);
1736
- }
1737
- refModelId = refModel.id ?? null;
1738
- }
1739
- // ADR 0008: a rename needs the property's id. A name that matches no stored property by id
1740
- // or name is a new property. The schema file is the source of truth for every column, and
1741
- // a property edited locally keeps its row.
1742
- const { row, changed } = await upsertPropertyRow(db, {
1743
- modelId: modelRecord.id,
1744
- name: propertyName,
1745
- dataType: propertyValues.dataType,
1746
- schemaFileId: propertyFileId ?? generateId(),
1747
- refModelId,
1748
- refValueType: propertyValues.refValueType ?? null,
1749
- required: propertyValues.required ?? false,
1750
- storage: propertyStorageColumns(propertyValues),
1751
- isEdited: false,
1752
- definition: schemaFileData?.propertyFileDefinitions?.get(modelName)?.[propertyName] ?? propertyValues,
1753
- replaceDefinition: true,
1754
- }, { keepStoredSchemaFileId: !propertyFileId, keepEditedRow: true });
1755
- if (changed && row?.schemaFileId)
1756
- updatedPropertyFileIds.push(row.schemaFileId);
1757
- }
1758
- }
1759
- catch (error) {
1760
- const errorMessage = error?.message || String(error);
1761
- logger$6(`Error processing model "${modelName}": ${errorMessage}`);
1762
- modelProcessingErrors.push({ modelName, error: errorMessage });
1763
- // Continue processing other models even if one fails
1764
- }
1765
- }
1766
- if (modelProcessingErrors.length > 0) {
1767
- console.warn(`[addModelsToDb] Errors occurred while processing ${modelProcessingErrors.length} model(s):`, modelProcessingErrors.map(e => `${e.modelName}: ${e.error}`).join('; '));
1768
- }
1769
- if (schemaRecord?.id) {
1770
- for (const [modelName, record] of modelRecords) {
1771
- if (!record.id)
1772
- continue;
1773
- if (await linkModelToSchemaRow(db, record.id, schemaRecord.id)) {
1774
- logger$6(`Linked model ${modelName} (id: ${record.id}) to schema (id: ${schemaRecord.id})`);
1775
- }
1776
- }
1777
- }
1778
- return updatedPropertyFileIds;
1779
- };
1780
- /**
1781
- * The models linked to a schema row (model_schemas) with their property rows, in schema-file
1782
- * shape: `{ [modelName]: { id, properties: { [name]: { dataType, schemaFileId, ref, ... } } } }`.
1783
- * Properties carry their row's definition (validation, description, ...); model-level fields only a
1784
- * schema file holds aren't here, see {@link mergeDbModelsIntoSchemaModels}.
1785
- * @param schemaId - The ID of the schema record in the database
1786
- */
1787
- const loadModelsFromDbForSchema = async (schemaId, db = BaseDb.getAppDb()) => {
1788
- if (!db) {
1789
- logger$6('Database not found, cannot load models from DB');
1790
- return {};
1791
- }
1792
- try {
1793
- const linked = await db
1794
- .select({ modelId: modelSchemas.modelId, modelName: models.name, schemaFileId: models.schemaFileId })
1795
- .from(modelSchemas)
1796
- .innerJoin(models, eq(modelSchemas.modelId, models.id))
1797
- .where(eq(modelSchemas.schemaId, schemaId));
1798
- const modelIds = linked.map((row) => row.modelId).filter((id) => id != null);
1799
- if (modelIds.length === 0)
1800
- return {};
1801
- const refModels = alias(models, 'ref_models');
1802
- const propertyRows = await db
1803
- .select({ property: properties, refName: refModels.name })
1804
- .from(properties)
1805
- .leftJoin(refModels, eq(properties.refModelId, refModels.id))
1806
- .where(inArray(properties.modelId, modelIds));
1807
- const models$1 = {};
1808
- const nameById = new Map();
1809
- for (const { modelId, modelName, schemaFileId } of linked) {
1810
- if (!modelId || !modelName)
1811
- continue;
1812
- nameById.set(modelId, modelName);
1813
- // The id (schemaFileId) lets Model.create find the existing row instead of adding a "Post 1".
1814
- models$1[modelName] = { properties: {}, ...(schemaFileId ? { id: schemaFileId } : {}) };
1815
- }
1816
- for (const { property: prop, refName } of propertyRows) {
1817
- const modelName = nameById.get(prop.modelId);
1818
- if (!modelName)
1819
- continue;
1820
- // The row's definition without its row keys; `required` stays the file's
1821
- const { id: _id, _dbId, name: _name, modelId: _modelId, refModelId: _refModelId, refModelName: _refModelName, required: _required, ...definition } = propertyDefinitionFromRow(prop, refName);
1822
- models$1[modelName].properties[prop.name] = {
1823
- ...definition,
1824
- ...(prop.schemaFileId ? { schemaFileId: prop.schemaFileId } : {}),
1825
- };
1826
- }
1827
- logger$6(`Loaded ${Object.keys(models$1).length} models from database for schema ${schemaId}`);
1828
- return models$1;
1829
- }
1830
- catch (error) {
1831
- logger$6(`Error loading models from database for schema ${schemaId}:`, error);
1832
- return {};
1833
- }
1834
- };
1835
- /**
1836
- * Merges the models {@link loadModelsFromDbForSchema} read from the database into a schema's
1837
- * models. The database decides which models and properties exist and their stored fields; the
1838
- * schema keeps what only it holds (nameMatch, description, validation, ...), found by id first so
1839
- * a property renamed in the database keeps them. A schema property whose id a database property
1840
- * has is dropped under its old name.
1841
- */
1842
- const mergeDbModelsIntoSchemaModels = (schemaModels, dbModels) => {
1843
- const merged = { ...(schemaModels ?? {}) };
1844
- for (const [modelName, dbModel] of Object.entries(dbModels)) {
1845
- const schemaModel = schemaModels?.[modelName];
1846
- if (!schemaModel) {
1847
- merged[modelName] = dbModel;
1848
- continue;
1849
- }
1850
- const schemaProps = schemaModel.properties ?? {};
1851
- const dbProps = dbModel.properties ?? {};
1852
- const schemaNameById = new Map();
1853
- for (const [name, prop] of Object.entries(schemaProps)) {
1854
- const id = prop?.id ?? prop?.schemaFileId;
1855
- if (id)
1856
- schemaNameById.set(id, name);
1857
- }
1858
- const properties = {};
1859
- const matched = new Set();
1860
- for (const [name, dbProp] of Object.entries(dbProps)) {
1861
- const byId = dbProp?.schemaFileId ? schemaNameById.get(dbProp.schemaFileId) : undefined;
1862
- const byName = schemaProps[name];
1863
- const byNameId = byName?.id ?? byName?.schemaFileId;
1864
- // By name only when the ids don't say it's another property.
1865
- const schemaName = byId ?? (byName && !(byNameId && dbProp?.schemaFileId && byNameId !== dbProp.schemaFileId) ? name : undefined);
1866
- const schemaProp = schemaName ? schemaProps[schemaName] : undefined;
1867
- if (schemaName && schemaProp)
1868
- matched.add(schemaName);
1869
- // The database's dataType and ref replace the file's type and model.
1870
- const { type, model, ...fromSchema } = schemaProp ?? {};
1871
- properties[name] = {
1872
- ...fromSchema,
1873
- ...(dbProp?.dataType === undefined && type !== undefined ? { type } : {}),
1874
- ...(dbProp?.ref === undefined && model !== undefined ? { model } : {}),
1875
- ...dbProp,
1876
- };
1877
- }
1878
- const unmatched = Object.fromEntries(Object.entries(schemaProps).filter(([name]) => !matched.has(name)));
1879
- merged[modelName] = { ...schemaModel, ...dbModel, properties: { ...unmatched, ...properties } };
1880
- }
1881
- return merged;
1882
- };
1883
- /**
1884
- * Saves a ModelProperty's edit to its properties row (is_edited true) without updating the schema
1885
- * file, in one transaction. A context without `required` or storage settings leaves the row's.
1886
- * @param property - The ModelPropertyMachineContext with updated values
1887
- */
1888
- const savePropertyToDb = async (property) => {
1889
- if (!BaseDb.getAppDb()) {
1890
- throw new Error('Database not found');
1891
- }
1892
- const name = property.name;
1893
- if (!name) {
1894
- throw new Error('Property name is required');
1895
- }
1896
- const schemaFileId = property._propertyFileId || (typeof property.id === 'string' ? property.id : undefined);
1897
- const storage = propertyStorageColumns(property);
1898
- await inDefinitionsTransaction(async (tx) => {
1899
- // modelId, then the model of the property row (by its schemaFileId), then modelName in the
1900
- // property's schema. context.modelName can also hold the model's schemaFileId.
1901
- const modelRecord = await resolveModelRecord(property.modelName, {
1902
- modelId: typeof property.modelId === 'number' ? property.modelId : undefined,
1903
- propertyFileId: schemaFileId,
1904
- modelFileId: property.modelName,
1905
- schemaName: property._schemaName,
1906
- }, tx);
1907
- if (!modelRecord) {
1908
- throw new Error(`Model not found in database (modelId=${property.modelId}, modelName=${property.modelName})`);
1909
- }
1910
- await upsertPropertyRow(tx, {
1911
- modelId: modelRecord.id,
1912
- name,
1913
- dataType: property.dataType,
1914
- schemaFileId,
1915
- previousName: property._originalValues?.name,
1916
- refModelName: property.refModelName,
1917
- refModelId: property.refModelName ? undefined : (property.refModelId ?? null),
1918
- refValueType: property.refValueType ?? null,
1919
- required: property.required,
1920
- storage: hasStorageSettings(storage) ? storage : undefined,
1921
- isEdited: true,
1922
- definition: property,
1923
- });
1924
- });
1925
- logger$6(`Saved property ${property.modelName}:${property.name}`);
1926
- };
1927
- function parseAddressConfig(value) {
1928
- if (!value)
1929
- return null;
1930
- try {
1931
- const parsed = JSON.parse(value);
1932
- if (Array.isArray(parsed)) {
1933
- return normalizeAddressConfig(parsed);
1934
- }
1935
- if (parsed && typeof parsed === 'object' && Array.isArray(parsed.owned)) {
1936
- return normalizeAddressConfig({
1937
- owned: parsed.owned,
1938
- watched: Array.isArray(parsed.watched) ? parsed.watched : [],
1939
- });
1940
- }
1941
- return null;
1942
- }
1943
- catch {
1944
- return null;
1945
- }
1946
- }
1947
- const getOwnedAddressesFromDb = async () => {
1948
- const config = await getAddressConfigFromDb();
1949
- return config?.owned ?? [];
1950
- };
1951
- const getWatchedAddressesFromDb = async () => {
1952
- const config = await getAddressConfigFromDb();
1953
- return config?.watched ?? [];
1954
- };
1955
- /**
1956
- * Returns persisted owned or watched addresses for list filters.
1957
- * Does not include `getAdditionalSyncAddresses` (those are extra EAS indexers).
1958
- */
1959
- const getAddressesForItemsFilter = async (addressFilter) => {
1960
- if (addressFilter === 'owned') {
1961
- return getOwnedAddressesFromDb();
1962
- }
1963
- return getWatchedAddressesFromDb();
1964
- };
1965
- /**
1966
- * Returns owned + watched addresses. Use for EAS sync and file download.
1967
- */
1968
- const getAllAddressesFromDb = async () => {
1969
- const config = await getAddressConfigFromDb();
1970
- if (!config)
1971
- return [];
1972
- return [...config.owned, ...config.watched];
1973
- };
1974
- async function getAddressConfigFromDb() {
1975
- // Before init opens the database (or with no init) there are no stored addresses to read
1976
- const appDb = await whenDbReady();
1977
- if (!appDb)
1978
- return null;
1979
- const appStatesRecords = await appDb
1980
- .select()
1981
- .from(appState)
1982
- .where(eq(appState.key, 'addresses'))
1983
- .limit(1);
1984
- if (!appStatesRecords || appStatesRecords.length === 0) {
1985
- return null;
1986
- }
1987
- const addressArrayString = appStatesRecords[0].value;
1988
- return parseAddressConfig(addressArrayString);
1989
- }
1990
- /**
1991
- * Writes a runtime-created model: its models row (is_edited true when new), its model_schemas link
1992
- * and the properties given, in one transaction. Returns the model's row id.
1993
- * @param modelFileId - The model file ID (schema_file_id)
1994
- * @param data - Model data including modelName, schemaId, and optional properties
1995
- */
1996
- async function writeModelToDb(modelFileId, data) {
1997
- if (!BaseDb.getAppDb())
1998
- throw new Error('Database not available');
1999
- if (!data.schemaId || !Number.isInteger(data.schemaId) || data.schemaId <= 0) {
2000
- throw new Error(`Invalid schemaId: ${data.schemaId}. Schema record must exist before creating join entry.`);
2001
- }
2002
- const { generateId } = await Promise.resolve().then(function () { return generateId$1; });
2003
- const { linked, schema, modelId } = await inDefinitionsTransaction(async (tx) => {
2004
- const [schema] = await tx
2005
- .select({ id: schemas.id, name: schemas.name, schemaFileId: schemas.schemaFileId })
2006
- .from(schemas)
2007
- .where(eq(schemas.id, data.schemaId))
2008
- .limit(1);
2009
- if (!schema) {
2010
- throw new Error(`Schema with id ${data.schemaId} does not exist in database. Cannot create join record.`);
2011
- }
2012
- const model = await upsertModelRow(tx, { name: data.modelName, schemaFileId: modelFileId, isEdited: true });
2013
- const linked = await linkModelToSchemaRow(tx, model.id, data.schemaId);
2014
- for (const [propName, propData] of Object.entries(data.properties ?? {})) {
2015
- const refModelName = propData.refModelName || propData.ref;
2016
- const storage = propertyStorageColumns(propData);
2017
- // A stored row keeps its id; the given or a generated one is for a new row.
2018
- await upsertPropertyRow(tx, {
2019
- modelId: model.id,
2020
- name: propName,
2021
- dataType: propData.dataType,
2022
- schemaFileId: propData.id ?? generateId(),
2023
- refModelName,
2024
- refModelId: refModelName ? undefined : (propData.refModelId ?? null),
2025
- refValueType: propData.refValueType ?? null,
2026
- required: propData.required,
2027
- storage: hasStorageSettings(storage) ? storage : undefined,
2028
- definition: propData,
2029
- }, { keepStoredSchemaFileId: true });
2030
- }
2031
- return { linked, schema, modelId: model.id };
2032
- });
2033
- if (linked)
2034
- broadcastModelsInvalidate(schema);
2035
- logger$6(`Wrote model ${data.modelName} (${modelFileId}) to database`);
2036
- return modelId;
2037
- }
2038
- /**
2039
- * Writes a property's row by its file id ({@link upsertPropertyRow}), in its own transaction.
2040
- * `required` and storage settings the data leaves out keep the row's.
2041
- * @param propertyFileId - The property file ID (schema_file_id)
2042
- * @param data - Property data including modelId, name, dataType, and other property fields
2043
- * @param isEdited - Optional flag indicating if property has been edited locally. Left unset, an update
2044
- * keeps the row's flag (a load or creation write must not clear an unsaved edit) and an insert stores false.
2045
- */
2046
- async function writePropertyToDb(propertyFileId, data, isEdited) {
2047
- if (!BaseDb.getAppDb())
2048
- throw new Error('Database not available');
2049
- const refModelName = data.refModelName || data.ref;
2050
- const storage = propertyStorageColumns(data);
2051
- await inDefinitionsTransaction((tx) => upsertPropertyRow(tx, {
2052
- modelId: data.modelId,
2053
- name: data.name,
2054
- dataType: data.dataType,
2055
- schemaFileId: propertyFileId,
2056
- refModelName,
2057
- refModelId: refModelName ? undefined : (data.refModelId ?? null),
2058
- refValueType: data.refValueType ?? null,
2059
- required: data.required,
2060
- storage: hasStorageSettings(storage) ? storage : undefined,
2061
- isEdited,
2062
- definition: data,
2063
- }));
2064
- logger$6(`Wrote property ${data.name} (${propertyFileId})`);
2065
- }
2066
-
2067
- var db = /*#__PURE__*/Object.freeze({
2068
- __proto__: null,
2069
- addModelsToDb: addModelsToDb,
2070
- addSchemaToDb: addSchemaToDb,
2071
- broadcastSchemasInvalidate: broadcastSchemasInvalidate,
2072
- getAddressesForItemsFilter: getAddressesForItemsFilter,
2073
- getAllAddressesFromDb: getAllAddressesFromDb,
2074
- getItemStoragePropertiesForModel: getItemStoragePropertiesForModel,
2075
- getOwnedAddressesFromDb: getOwnedAddressesFromDb,
2076
- getWatchedAddressesFromDb: getWatchedAddressesFromDb,
2077
- loadModelsFromDbForSchema: loadModelsFromDbForSchema,
2078
- mergeDbModelsIntoSchemaModels: mergeDbModelsIntoSchemaModels,
2079
- propertyStorageColumns: propertyStorageColumns,
2080
- renameModelInDb: renameModelInDb,
2081
- renamePendingWritesForProperty: renamePendingWritesForProperty,
2082
- savePropertyToDb: savePropertyToDb,
2083
- writeModelToDb: writeModelToDb,
2084
- writePropertyToDb: writePropertyToDb
2085
- });
2086
-
2087
- /**
2088
- * Phase timings for item creates, finds and property saves, and ready waits that timed out but
2089
- * were allowed to continue, under `DEBUG=seedSdk:timing`. Timing lines are `<label> <ms>ms` plus
2090
- * any details, so an app can report where a slow call spent its time.
2091
- */
2092
- const timingLogger = debug('seedSdk:timing');
2093
- const now = () => (typeof performance !== 'undefined' ? performance.now() : Date.now());
2094
- /** Runs `fn` and logs how long it took (also when it throws). Costs nothing when the namespace is off. */
2095
- async function timed(label, fn, details) {
2096
- if (!timingLogger.enabled)
2097
- return fn();
2098
- const start = now();
2099
- let failed = false;
2100
- try {
2101
- return await fn();
2102
- }
2103
- catch (error) {
2104
- failed = true;
2105
- throw error;
2106
- }
2107
- finally {
2108
- const extra = details?.();
2109
- timingLogger(`${label} ${Math.round(now() - start)}ms${failed ? ' (failed)' : ''}${extra ? ` ${extra}` : ''}`);
2110
- }
2111
- }
2112
- /**
2113
- * Database work counted while the namespace is on, so a timing line can say how much of its time
2114
- * was the SQLite worker. Statements are the ones the SDK sends through the driver; their time runs
2115
- * from send to result, so it includes the wait behind everything queued before them on the one
2116
- * worker. Live-query runs are SQLocal reactive-query results (the first run and every re-run after
2117
- * a write to a watched table), which go through the same worker but not through the driver.
2118
- */
2119
- const dbTimingCounters = {
2120
- statements: 0,
2121
- statementMs: 0,
2122
- inFlight: 0,
2123
- liveQueryRuns: 0,
2124
- liveQueryFirstRuns: 0,
2125
- liveQueryFirstRunMs: 0,
2126
- };
2127
- const snapshotCounters = () => ({ ...dbTimingCounters });
2128
- /** Wraps a driver function so each call is counted while timing is on. */
2129
- function countDbStatements(driver) {
2130
- return async (...args) => {
2131
- if (!timingLogger.enabled)
2132
- return driver(...args);
2133
- const start = now();
2134
- dbTimingCounters.inFlight += 1;
2135
- try {
2136
- return await driver(...args);
2137
- }
2138
- finally {
2139
- dbTimingCounters.inFlight -= 1;
2140
- dbTimingCounters.statements += 1;
2141
- dbTimingCounters.statementMs += now() - start;
2142
- }
2143
- };
2144
- }
2145
- /** Starts the clock for one live query; call the result on each of its runs. */
2146
- function countLiveQueryRuns() {
2147
- if (!timingLogger.enabled)
2148
- return () => { };
2149
- const start = now();
2150
- let first = true;
2151
- return () => {
2152
- dbTimingCounters.liveQueryRuns += 1;
2153
- if (!first)
2154
- return;
2155
- first = false;
2156
- dbTimingCounters.liveQueryFirstRuns += 1;
2157
- dbTimingCounters.liveQueryFirstRunMs += now() - start;
2158
- };
2159
- }
2160
- const describeDbDelta = (before) => {
2161
- const after = dbTimingCounters;
2162
- const statements = after.statements - before.statements;
2163
- const statementMs = after.statementMs - before.statementMs;
2164
- const liveRuns = after.liveQueryRuns - before.liveQueryRuns;
2165
- const firstRuns = after.liveQueryFirstRuns - before.liveQueryFirstRuns;
2166
- const firstMs = after.liveQueryFirstRunMs - before.liveQueryFirstRunMs;
2167
- const parts = [`queued ${before.inFlight}`];
2168
- if (statements)
2169
- parts.push(`${statements} stmts avg ${Math.round(statementMs / statements)}ms`);
2170
- if (liveRuns)
2171
- parts.push(`${liveRuns} live runs`);
2172
- if (firstRuns)
2173
- parts.push(`${firstRuns} new live queries, first run avg ${Math.round(firstMs / firstRuns)}ms`);
2174
- return `[db: ${parts.join(', ')}]`;
2175
- };
2176
- /**
2177
- * Like `timed`, and the line also says how many statements and live-query runs went through the
2178
- * worker meanwhile (from any caller, since they share the queue) and how many statements were
2179
- * already queued when it started.
2180
- */
2181
- async function timedDb(label, fn, details) {
2182
- if (!timingLogger.enabled)
2183
- return fn();
2184
- const before = snapshotCounters();
2185
- return timed(label, fn, () => [details?.(), describeDbDelta(before)].filter(Boolean).join(' '));
2186
- }
2187
- /** Milliseconds since `start` (a `timingNow()` value), rounded. */
2188
- const timingSince = (start) => Math.round(now() - start);
2189
- const timingNow = now;
2190
- /**
2191
- * Split timer for one multi-step call: `mark(step)` records the time since the previous mark, and
2192
- * `log(label)` writes one line with the total and every step. Does nothing when timing is off.
2193
- */
2194
- function timingMarks() {
2195
- if (!timingLogger.enabled)
2196
- return { mark: () => { }, log: () => { } };
2197
- const start = now();
2198
- const before = snapshotCounters();
2199
- let last = start;
2200
- const steps = [];
2201
- return {
2202
- mark: (step) => {
2203
- const t = now();
2204
- steps.push(`${step} ${Math.round(t - last)}`);
2205
- last = t;
2206
- },
2207
- log: (label) => {
2208
- timingLogger(`${label} ${Math.round(now() - start)}ms: ${steps.join(', ')} ${describeDbDelta(before)}`);
2209
- },
2210
- };
2211
- }
2212
-
2213
- const randomId = customAlphabet(alphanumeric, 10);
2214
- /**
2215
- * A 10-character local id. Never starts with "0x" (about 1 in 3,844 random ids would), so code that
2216
- * tells local ids from EAS uids by that prefix can't mistake one for the other.
2217
- */
2218
- const generateId = () => {
2219
- let id = randomId();
2220
- while (id.startsWith('0x'))
2221
- id = randomId();
2222
- return id;
2223
- };
2224
-
2225
- var generateId$1 = /*#__PURE__*/Object.freeze({
2226
- __proto__: null,
2227
- generateId: generateId
2228
- });
2229
-
2230
- const isNode = () => {
2231
- return (typeof process !== 'undefined' &&
2232
- process.versions != null &&
2233
- process.versions.node != null);
2234
- };
2235
- const isBrowser = () => {
2236
- return !isElectron() && typeof document !== 'undefined' && typeof window !== 'undefined';
2237
- };
2238
- const isElectron = () => {
2239
- return typeof process !== 'undefined' && process.versions != null && process.versions.electron != null;
2240
- };
2241
- /**
2242
- * Detects if code is running in an Electron renderer process.
2243
- *
2244
- * Uses multiple detection methods for robustness:
2245
- * 1. Checks process.type === 'renderer' (most reliable)
2246
- * 2. Falls back to checking if Electron is present and window/document exist
2247
- *
2248
- * @returns true if running in Electron renderer process, false otherwise
2249
- */
2250
- const isElectronRenderer = () => {
2251
- // Method 1: Check process.type (most reliable - official Electron API)
2252
- if (typeof process !== 'undefined' &&
2253
- process.type === 'renderer') {
2254
- return true;
2255
- }
2256
- // Method 2: Fallback - Electron + browser-like environment
2257
- // Renderer processes have window/document, main process doesn't (in modern Electron)
2258
- if (isElectron() &&
2259
- typeof window !== 'undefined' &&
2260
- typeof document !== 'undefined') {
2261
- return true;
2262
- }
2263
- return false;
2264
- };
2265
- /**
2266
- * True where OPFS-backed Arweave file downloads should run: normal browsers and
2267
- * Electron renderer. {@link isBrowser} is false for every Electron build (including renderer).
2268
- */
2269
- const supportsOpfsFileDownloads = () => {
2270
- return isBrowser() || isElectronRenderer();
2271
- };
2272
- /**
2273
- * NODE_ENV is 'test' or IS_SEED_DEV is set. Bundlers compile `process.env.NODE_ENV` in, but
2274
- * not IS_SEED_DEV, so that is only read where a `process` global exists: browsers have none.
2275
- */
2276
- const isTestOrSeedDev = () => process.env.NODE_ENV === 'test' || (typeof process !== 'undefined' && !!process.env?.IS_SEED_DEV);
2277
-
2278
- /**
2279
- * Cross-tab exclusive sections, using the Web Locks API. All tabs of an origin share one SQLite
2280
- * database and OPFS tree; the browser releases a tab's locks when it closes or crashes, so a lock
2281
- * can't go stale. See docs/MULTI_TAB.md.
2282
- *
2283
- * Node has Web Locks from 24.5, scoped to the process. Where they're missing (older Node, old
2284
- * browsers), sections still run one at a time within this JS realm (see `withProcessLock`), which
2285
- * covers a single Node process but not other tabs or worker threads.
2286
- * Not re-entrant: requesting a name the caller already holds waits forever, so take each lock
2287
- * only at the outermost call site.
2288
- */
2289
- /** Long enough for a slow init in another tab; short enough that a hung tab doesn't block forever. */
2290
- const TAB_LOCK_WAIT_TIMEOUT_MS = 60000;
2291
- function seedDbLockName(scope, filesDir) {
2292
- return `seed:${scope}:${filesDir}/db/seed.db`;
2293
- }
2294
- /** `withTabLock` on the database under `filesDir`; runs `fn` directly when there's no `filesDir` yet. */
2295
- function withSeedDbLock(scope, filesDir, fn) {
2296
- return filesDir ? withTabLock(seedDbLockName(scope, filesDir), fn) : fn();
2297
- }
2298
- class TabLockTimeoutError extends Error {
2299
- constructor(lockName, timeoutMs) {
2300
- super(`Timed out after ${timeoutMs}ms waiting for another tab to release "${lockName}".`);
2301
- this.lockName = lockName;
2302
- this.code = 'TAB_LOCK_TIMEOUT';
2303
- this.name = 'TabLockTimeoutError';
2304
- }
2305
- }
2306
- async function withTabLock(name, fn, { timeoutMs = TAB_LOCK_WAIT_TIMEOUT_MS } = {}) {
2307
- const locks = typeof navigator !== 'undefined' ? navigator.locks : undefined;
2308
- if (!locks)
2309
- return withProcessLock(name, fn, { timeoutMs });
2310
- try {
2311
- // The timeout bounds the wait only; once granted, fn runs to completion. Infinity waits for as
2312
- // long as the holder runs (it can't outlive its tab).
2313
- const signal = Number.isFinite(timeoutMs) ? AbortSignal.timeout(timeoutMs) : undefined;
2314
- return await locks.request(name, { mode: 'exclusive', signal }, fn);
2315
- }
2316
- catch (error) {
2317
- if (error?.name === 'TimeoutError') {
2318
- throw new TabLockTimeoutError(name, timeoutMs);
2319
- }
2320
- throw error;
2321
- }
2322
- }
2323
- /**
2324
- * Runs `fn` holding lock `name` only if no one holds it or waits for it now; otherwise runs nothing
2325
- * and returns `{ acquired: false }`. For work that has something else to do meanwhile (the outbox
2326
- * drainer).
2327
- */
2328
- async function tryWithTabLock(name, fn) {
2329
- const locks = typeof navigator !== 'undefined' ? navigator.locks : undefined;
2330
- if (!locks) {
2331
- if (processLockTails.has(name))
2332
- return { acquired: false };
2333
- return { acquired: true, value: await withProcessLock(name, fn) };
2334
- }
2335
- return locks.request(name, { mode: 'exclusive', ifAvailable: true }, async (lock) => lock ? { acquired: true, value: await fn() } : { acquired: false });
2336
- }
2337
- /** Tail of each name's queue: resolves once every section queued so far has finished or given up. */
2338
- const processLockTails = new Map();
2339
- /**
2340
- * The `withTabLock` fallback without Web Locks: an exclusive, first-come-first-served lock per name
2341
- * within this JS realm. Like Web Locks, `timeoutMs` bounds the wait only, and a waiter that times
2342
- * out doesn't let later waiters in before the current holder finishes.
2343
- */
2344
- async function withProcessLock(name, fn, { timeoutMs = TAB_LOCK_WAIT_TIMEOUT_MS } = {}) {
2345
- const previous = processLockTails.get(name) ?? Promise.resolve();
2346
- let release;
2347
- const done = new Promise((resolve) => (release = resolve));
2348
- const tail = previous.then(() => done);
2349
- processLockTails.set(name, tail);
2350
- void tail.then(() => {
2351
- if (processLockTails.get(name) === tail)
2352
- processLockTails.delete(name);
2353
- });
2354
- try {
2355
- await waitForTurn(previous, name, timeoutMs);
2356
- }
2357
- catch (error) {
2358
- release();
2359
- throw error;
2360
- }
2361
- try {
2362
- return await fn();
2363
- }
2364
- finally {
2365
- release();
2366
- }
2367
- }
2368
- function waitForTurn(previous, name, timeoutMs) {
2369
- if (!Number.isFinite(timeoutMs))
2370
- return previous;
2371
- let timer;
2372
- const timedOut = new Promise((_, reject) => {
2373
- timer = setTimeout(() => reject(new TabLockTimeoutError(name, timeoutMs)), timeoutMs);
2374
- });
2375
- return Promise.race([previous, timedOut]).finally(() => clearTimeout(timer));
2376
- }
2377
-
2378
- const logger$5 = debug('seedSdk:helpers:tabCoordinator');
2379
- /** Identifies this tab (this page's SDK instance) in the messages it sends. */
2380
- const TAB_ID = typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function'
2381
- ? crypto.randomUUID()
2382
- : `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
2383
- let state;
2384
- let leader = false;
2385
- let leaderWaiters = [];
2386
- const messageHandlers = new Set();
2387
- /** Starts once per page until stopTabCoordination; later calls (re-init) keep the first database. */
2388
- function startTabCoordination({ filesDir, mode = 'coordinate' }) {
2389
- if (state)
2390
- return;
2391
- const locks = typeof navigator !== 'undefined' ? navigator.locks : undefined;
2392
- const dbKey = `${filesDir}/db/seed.db`;
2393
- const coordinating = mode === 'coordinate' && isBrowser() && !!locks && typeof BroadcastChannel !== 'undefined';
2394
- state = { dbKey, coordinating };
2395
- if (!coordinating) {
2396
- becomeLeader();
2397
- return;
2398
- }
2399
- const channel = new BroadcastChannel(`seed:tabs:${dbKey}`);
2400
- channel.onmessage = (event) => {
2401
- // A BroadcastChannel never hears itself, but another channel object in this page would (a
2402
- // second channel opened by a test, or the same module loaded twice): skip our own messages.
2403
- if (event.data?.from === TAB_ID)
2404
- return;
2405
- for (const handler of messageHandlers)
2406
- handler(event.data);
2407
- };
2408
- state.channel = channel;
2409
- const current = state;
2410
- void locks.request(`seed:leader:${dbKey}`, () => {
2411
- // Reset (tests) before the grant: hand the lock straight back.
2412
- if (state !== current)
2413
- return undefined;
2414
- becomeLeader();
2415
- // Held until the tab closes (or a test resets coordination).
2416
- return new Promise((release) => {
2417
- current.releaseLeadership = release;
2418
- });
2419
- });
2420
- }
2421
- function becomeLeader() {
2422
- if (leader)
2423
- return;
2424
- leader = true;
2425
- logger$5(`this tab leads ${state?.dbKey}`);
2426
- const waiters = leaderWaiters;
2427
- leaderWaiters = [];
2428
- for (const resolve of waiters)
2429
- resolve();
2430
- }
2431
- function isLeaderTab() {
2432
- return !state || leader;
2433
- }
2434
- /** Resolves when this tab leads (at once if it already does). */
2435
- function whenLeaderTab() {
2436
- if (isLeaderTab())
2437
- return Promise.resolve();
2438
- return new Promise((resolve) => leaderWaiters.push(resolve));
2439
- }
2440
- /** Sends to the other tabs of this database; a no-op when not coordinating. */
2441
- function postTabMessage(message) {
2442
- state?.channel?.postMessage({ ...message, from: TAB_ID });
2443
- }
2444
- /** Whether postTabMessage reaches other tabs (a browser tab coordinating with Web Locks). */
2445
- function isPostingTabMessages() {
2446
- return !!state?.channel;
2447
- }
2448
- function onTabMessage(handler) {
2449
- messageHandlers.add(handler);
2450
- return () => messageHandlers.delete(handler);
2451
- }
2452
- /** `withTabLock` named for this database (`seed:<scope>:<db>`); runs `fn` directly before init. */
2453
- function withDbTabLock(scope, fn, options) {
2454
- return state ? withTabLock(`seed:${scope}:${state.dbKey}`, fn, options) : fn();
2455
- }
2456
- /**
2457
- * Ends this tab's coordination (client.stop): hands leadership to another tab and closes the
2458
- * channel. The next startTabCoordination starts over, for whatever database init then opens.
2459
- * Keeps message handlers (modules register theirs once per page) and whenLeaderTab waiters, which
2460
- * resolve once this tab leads again.
2461
- */
2462
- function stopTabCoordination() {
2463
- state?.releaseLeadership?.();
2464
- state?.channel?.close();
2465
- state = undefined;
2466
- leader = false;
2467
- }
2468
-
2469
- const logger$4 = debug('seedSdk:helpers:localImageBlobUrl');
2470
- /**
2471
- * A new blob: URL for `images/<txId>` in the files directory, or undefined when the file isn't
2472
- * there. Each call creates a URL; the caller owns it and revokes it with `URL.revokeObjectURL`
2473
- * once nothing displays it.
2474
- */
2475
- const convertTxIdToImage = async (txId) => {
2476
- const imageFilePath = BaseFileManager.getFilesPath('images', txId);
2477
- const fileExists = await BaseFileManager.pathExists(imageFilePath);
2478
- if (!fileExists) {
2479
- logger$4(`${imageFilePath} does not exist`);
2480
- return;
2481
- }
2482
- const buffer = await BaseFileManager.readFileAsBuffer(imageFilePath);
2483
- // Convert Blob to ArrayBuffer if needed, or use Buffer directly
2484
- let arrayBuffer;
2485
- if (buffer instanceof Blob) {
2486
- arrayBuffer = await buffer.arrayBuffer();
2487
- }
2488
- else if (buffer instanceof ArrayBuffer) {
2489
- arrayBuffer = buffer;
2490
- }
2491
- else {
2492
- // Node.js Buffer
2493
- const sliced = buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
2494
- // Handle SharedArrayBuffer case by creating a new ArrayBuffer
2495
- if (sliced instanceof SharedArrayBuffer) {
2496
- const uint8Array = new Uint8Array(sliced);
2497
- arrayBuffer = uint8Array.buffer.slice(0);
2498
- }
2499
- else {
2500
- arrayBuffer = sliced;
2501
- }
2502
- }
2503
- return URL.createObjectURL(new Blob([new Uint8Array(arrayBuffer)]));
2504
- };
2505
-
2506
- /**
2507
- * Tag every Seed upload carries with the hex digest of its data. Producers: the SDK's
2508
- * `getPublishUploadData` / `getPublishUploads` (and through them both the L1 transaction path and
2509
- * the bundler / DataItem path in `@seedprotocol/publish`). Consumer: the files download
2510
- * (`events/files/download.ts` and the download worker), which verifies downloaded bytes against it.
2511
- */
2512
- const CONTENT_SHA256_TAG = 'Content-SHA-256';
2513
- const CONTENT_TYPE_TAG = 'Content-Type';
2514
- /**
2515
- * Lowercase hex SHA-256 of `data`: the value of the {@link CONTENT_SHA256_TAG} tag.
2516
- *
2517
- * Uses @noble/hashes rather than WebCrypto so it also works outside secure contexts (WebCrypto's
2518
- * `crypto.subtle` is missing on plain-http origins); the digest is the same.
2519
- *
2520
- * In SDK 0.7.2 and earlier the L1 transaction path (`getPublishUploads`) wrote SHA3-256 under this tag name
2521
- * while the bundler path wrote SHA-256. Uploads from that path still carry SHA3-256 digests; use
2522
- * {@link contentSha256TagMatches} to check a tag value against bytes.
2523
- */
2524
- const getContentSha256 = (data) => bytesToHex(sha256(data));
2525
- /**
2526
- * True when `tagValue` (a {@link CONTENT_SHA256_TAG} value read from Arweave) is the digest of
2527
- * `data`. Accepts SHA-256 and, for uploads made by SDK 0.7.2 and earlier through the L1 transaction path,
2528
- * SHA3-256.
2529
- */
2530
- const contentSha256TagMatches = (data, tagValue) => {
2531
- const expected = tagValue.trim().toLowerCase();
2532
- if (!expected)
2533
- return false;
2534
- return getContentSha256(data) === expected || bytesToHex(sha3_256(data)) === expected;
2535
- };
2536
- /**
2537
- * Tags for one upload, in the order every upload uses: content hash, content type, then any
2538
- * configured extra tags (e.g. App-Name from `arweaveUploadTags`).
2539
- */
2540
- const buildArweaveContentTags = (contentHash, contentType, extra) => {
2541
- const tags = [];
2542
- if (contentHash)
2543
- tags.push({ name: CONTENT_SHA256_TAG, value: contentHash });
2544
- if (contentType)
2545
- tags.push({ name: CONTENT_TYPE_TAG, value: contentType });
2546
- if (extra?.length)
2547
- tags.push(...extra);
2548
- return tags;
2549
- };
2550
-
2551
- /** Subdirectory of the files root that holds a storage seed's file, by its data type. */
2552
- const getStorageSubdirForDataType = (dataType) => {
2553
- switch (normalizeDataType(dataType ?? undefined)) {
2554
- case 'Image':
2555
- return 'images';
2556
- case 'Html':
2557
- return 'html';
2558
- case 'Json':
2559
- return 'json';
2560
- case 'File':
2561
- default:
2562
- // Unknown types are generic files; `images/` is only for what the SDK knows is an image.
2563
- return 'files';
2564
- }
2565
- };
2566
- /**
2567
- * Subdirectory (relative to the files root, no leading slash) for an ItemStorage property's file:
2568
- * its model's `storage.path` (`localStorageDir`), else `files`.
2569
- */
2570
- const getItemStorageSubdir = (localStorageDir) => (localStorageDir ?? '').replace(/^\/+/, '').replace(/\/+$/, '') || 'files';
2571
- /**
2572
- * File name a local save gives an ItemStorage property: `<seedUid or seedLocalId><extension>`
2573
- * (see saveItemStorage). Downloaded content is named after its transaction instead
2574
- * (`<txId><extension>`, see syncDbWithEas), which is what the metadata row's refResolvedValue says.
2575
- */
2576
- const getItemStorageLocalFileName = (seedId, filenameSuffix) => `${seedId}${filenameSuffix ?? ''}`;
2577
-
2578
- /** Rows per query: well under SQLite's bound-parameter limit. */
2579
- const CHUNK_SIZE = 400;
2580
- /**
2581
- * Where the download worker saves each child of a composite upload, by transaction id then child
2582
- * (property) name: a path relative to the files root.
2583
- *
2584
- * The paths come from the ItemStorage rows sync derives from a `storage_transaction_id`
2585
- * attestation (see `createMetadataRecordsForStorageTransactionId`): property value = the
2586
- * transaction id, `refResolvedValue` = `<txId><extension>`, `localStorageDir` = the model's storage
2587
- * directory. Those rows are what hydration reads, so a child saved here is found as that property's
2588
- * file. Transactions with no such rows are left out; the worker then falls back to the legacy
2589
- * html / json layout.
2590
- */
2591
- const getCompositeChildPaths = async (transactionIds) => {
2592
- const out = {};
2593
- const appDb = BaseDb.getAppDb();
2594
- if (!appDb || transactionIds.length === 0)
2595
- return out;
2596
- for (let i = 0; i < transactionIds.length; i += CHUNK_SIZE) {
2597
- const chunk = transactionIds.slice(i, i + CHUNK_SIZE);
2598
- const rows = await appDb
2599
- .select({
2600
- transactionId: metadata.propertyValue,
2601
- propertyName: metadata.propertyName,
2602
- refResolvedValue: metadata.refResolvedValue,
2603
- localStorageDir: metadata.localStorageDir,
2604
- })
2605
- .from(metadata)
2606
- .where(and(isNotNull(metadata.derivedFromUid), isNotNull(metadata.refResolvedValue), inArray(metadata.propertyValue, chunk)));
2607
- for (const row of rows) {
2608
- const { transactionId, propertyName, refResolvedValue } = row;
2609
- if (!transactionId || !propertyName || !refResolvedValue)
2610
- continue;
2611
- // A file name, never a path: anything else is not where the SDK would look.
2612
- if (refResolvedValue.includes('/') || refResolvedValue.includes('\\'))
2613
- continue;
2614
- const byName = (out[transactionId] ?? (out[transactionId] = {}));
2615
- byName[propertyName] ?? (byName[propertyName] = `${getItemStorageSubdir(row.localStorageDir)}/${refResolvedValue}`);
2616
- }
2617
- }
2618
- return out;
2619
- };
2620
-
2621
- const logger$3 = debug('seedSdk:files:download');
2622
- const AUTO_BULK_DOWNLOAD_THROTTLE_MS = 10000;
2623
- /** Avoid refetch-on-every-call: transient empty indexer responses were overwriting good cache (see trailing throttle ~10s after success). */
2624
- const GET_FILES_METADATA_STALE_MS = 120000;
2625
- let bulkDownloadInFlight = null;
2626
- const lazyDownloadInFlightByTransactionId = new Map();
2627
- const getExcludedTransactions = async () => {
2628
- const appDb = await whenDbReady();
2629
- if (!appDb) {
2630
- return new Set();
2631
- }
2632
- const excludedTransactionsQuery = await appDb
2633
- .select()
2634
- .from(appState)
2635
- .where(eq(appState.key, 'excludedTransactions'));
2636
- const excludedTransactions = new Set();
2637
- if (excludedTransactionsQuery && excludedTransactionsQuery.length === 1) {
2638
- const valueString = excludedTransactionsQuery[0].value;
2639
- if (valueString) {
2640
- const excludedTransactionsArray = JSON.parse(valueString);
2641
- for (const txId of excludedTransactionsArray) {
2642
- excludedTransactions.add(txId);
2643
- }
2644
- }
2645
- }
2646
- return excludedTransactions;
2647
- };
2648
- /** Arweave transaction ids are 43 chars in the URL-safe base64 alphabet. */
2649
- const ARWEAVE_TX_ID_RE$1 = /^[a-z0-9_-]{43}$/i;
2650
- const normalizeArweaveTxId = (raw) => {
2651
- if (raw == null || raw === '')
2652
- return undefined;
2653
- const t = String(raw).trim();
2654
- if (ARWEAVE_TX_ID_RE$1.test(t))
2655
- return t;
2656
- const m = t.match(/^([a-z0-9_-]{43})(?:\.[^/]+)?$/i);
2657
- if (m)
2658
- return m[1];
2659
- // Values may be full gateway URLs or paths; scan for an embedded tx id. It must be 43 characters
2660
- // on its own, not the start of a longer run: the first 43 of a bytes32 value (`0x0000…0020`,
2661
- // an EAS UID) are in the alphabet too.
2662
- const embedded = t.match(/(?<![a-z0-9_-])[a-z0-9_-]{43}(?![a-z0-9_-])/gi);
2663
- return embedded?.[0];
2664
- };
2665
- const mergeAddressLists = (a, b) => {
2666
- const seen = new Set();
2667
- const out = [];
2668
- for (const addr of [...(a ?? []), ...(b ?? [])]) {
2669
- if (!addr || !addr.trim())
2670
- continue;
2671
- const k = addr.toLowerCase();
2672
- if (seen.has(k))
2673
- continue;
2674
- seen.add(k);
2675
- out.push(addr);
2676
- }
2677
- return out;
2678
- };
2679
- const collectTransactionIdsFromLocalMetadata = async () => {
2680
- const appDb = await whenDbReady();
2681
- if (!appDb) {
2682
- return [];
2683
- }
2684
- const nameOr = or(eq(metadata.propertyName, 'storageTransactionId'), eq(metadata.propertyName, 'storage_transaction_id'), eq(metadata.propertyName, 'transactionId'));
2685
- const propRows = await appDb
2686
- .select({ propertyValue: metadata.propertyValue })
2687
- .from(metadata)
2688
- .where(and(nameOr, isNotNull(metadata.propertyValue)));
2689
- const refRows = await appDb
2690
- .select({ refResolvedValue: metadata.refResolvedValue })
2691
- .from(metadata)
2692
- .where(isNotNull(metadata.refResolvedValue));
2693
- const ids = new Set();
2694
- for (const row of propRows) {
2695
- const id = normalizeArweaveTxId(row.propertyValue ?? undefined);
2696
- if (id)
2697
- ids.add(id);
2698
- }
2699
- for (const row of refRows) {
2700
- const id = normalizeArweaveTxId(row.refResolvedValue ?? undefined);
2701
- if (id)
2702
- ids.add(id);
2703
- }
2704
- return Array.from(ids);
2705
- };
2706
- const parseTransactionIdsFromMetadata = (filesMetadata, excludedTransactions) => {
2707
- const transactionIds = [];
2708
- for (const fileMetadata of filesMetadata) {
2709
- // Validate and parse decodedDataJson
2710
- if (!fileMetadata.decodedDataJson || fileMetadata.decodedDataJson.trim() === '') {
2711
- console.warn('[events/files] [download] empty decodedDataJson for fileMetadata: ', fileMetadata.id);
2712
- continue;
2713
- }
2714
- let json;
2715
- try {
2716
- json = JSON.parse(fileMetadata.decodedDataJson);
2717
- }
2718
- catch (error) {
2719
- console.warn('[events/files] [download] failed to parse decodedDataJson for fileMetadata: ', fileMetadata.id, error);
2720
- continue;
2721
- }
2722
- if (!Array.isArray(json) || json.length === 0 || !json[0]?.value?.value) {
2723
- console.warn('[events/files] [download] invalid decodedDataJson structure for fileMetadata: ', fileMetadata.id);
2724
- continue;
2725
- }
2726
- const rawTx = json[0].value.value;
2727
- if (typeof rawTx !== 'string' || rawTx.trim().length === 0) {
2728
- continue;
2729
- }
2730
- const transactionId = normalizeArweaveTxId(rawTx.trim());
2731
- if (!transactionId) {
2732
- continue;
2733
- }
2734
- if (excludedTransactions.has(transactionId) || excludedTransactions.has(rawTx.trim())) {
2735
- continue;
2736
- }
2737
- transactionIds.push(transactionId);
2738
- }
2739
- return transactionIds;
2740
- };
2741
- /** How long looked-up transaction tags stay cached. */
2742
- const TRANSACTION_TAGS_CACHE_MS = 5 * 60 * 1000;
2743
- /** Content-SHA-256 tag values for the transactions that have one, from one batched tag lookup (cached). */
2744
- const getExpectedContentHashes = async (transactionIds) => {
2745
- const queryClient = BaseQueryClient.getQueryClient();
2746
- const tagsById = new Map();
2747
- const toFetch = [];
2748
- for (const transactionId of transactionIds) {
2749
- // Tags never change once a transaction is indexed, so a cached lookup stays good.
2750
- const cached = queryClient.getQueryData?.(['getTransactionTags', transactionId]);
2751
- if (cached)
2752
- tagsById.set(transactionId, cached);
2753
- else
2754
- toFetch.push(transactionId);
2755
- }
2756
- if (toFetch.length > 0) {
2757
- try {
2758
- const fetched = await getTransactionTagsByIds(toFetch, {
2759
- client: BaseArweaveClient.getArweaveClient(),
2760
- onError: (error) => logger$3('[download] transaction tags lookup failed', error),
2761
- });
2762
- for (const [transactionId, tags] of fetched) {
2763
- tagsById.set(transactionId, tags);
2764
- // Seed the per-transaction cache entry (the client interface has no setQueryData).
2765
- void queryClient
2766
- .fetchQuery({
2767
- queryKey: ['getTransactionTags', transactionId],
2768
- queryFn: async () => tags,
2769
- // The query client keeps a result only as long as the caller's staleTime.
2770
- staleTime: TRANSACTION_TAGS_CACHE_MS,
2771
- })
2772
- .catch(() => { });
2773
- }
2774
- }
2775
- catch (error) {
2776
- logger$3('[download] transaction tags lookup failed', error);
2777
- }
2778
- }
2779
- const out = {};
2780
- for (const [transactionId, tags] of tagsById) {
2781
- const value = tags.find((tag) => tag.name === CONTENT_SHA256_TAG)?.value?.trim();
2782
- if (value)
2783
- out[transactionId] = value;
2784
- }
2785
- return out;
2786
- };
2787
- const downloadTransactionIds = async (rawTransactionIds, options) => {
2788
- if (!supportsOpfsFileDownloads()) {
2789
- return false;
2790
- }
2791
- const transactionIds = Array.from(new Set(rawTransactionIds.filter((tx) => !!tx && tx.trim().length > 0)));
2792
- if (transactionIds.length === 0) {
2793
- return false;
2794
- }
2795
- const excludedTransactions = await getExcludedTransactions();
2796
- await ensureReadGatewaySelected();
2797
- // Browser: do not call getTransactionStatus() — many gateways (e.g. g8way.io) redirect or omit
2798
- // CORS headers, so fetch() fails and we would wrongly exclude txs. The OPFS worker fetch decides success.
2799
- const transactionIdsToDownload = transactionIds.filter((id) => !excludedTransactions.has(id));
2800
- if (transactionIdsToDownload.length === 0) {
2801
- return false;
2802
- }
2803
- // One batched lookup for every upload's Content-SHA-256 tag; downloads are verified against it.
2804
- // Missing tags (old uploads, or a just-published one not indexed yet) just mean no verification.
2805
- const expectedContentHashes = await getExpectedContentHashes(transactionIdsToDownload);
2806
- // Preferred gateways first: a just-published item is only on the gateway it was uploaded through.
2807
- const arweaveBaseUrls = getArweaveReadBaseUrls();
2808
- // Where each ItemStorage child of a composite goes (the worker cannot read the DB or the schema).
2809
- const compositeChildPaths = await getCompositeChildPaths(transactionIdsToDownload).catch((error) => {
2810
- logger$3('[download] composite child paths lookup failed', error);
2811
- return {};
2812
- });
2813
- let result;
2814
- if (transactionIdsToDownload.length === 1) {
2815
- const transactionId = transactionIdsToDownload[0];
2816
- result = await BaseFileManager.downloadFileByTransactionId({
2817
- transactionId,
2818
- arweaveHost: BaseArweaveClient.getBaseUrl(),
2819
- arweaveBaseUrls,
2820
- excludedTransactions,
2821
- expectedContentHash: expectedContentHashes[transactionId],
2822
- compositeChildPaths: compositeChildPaths[transactionId],
2823
- skipExisting: options?.skipExisting,
2824
- });
2825
- }
2826
- else {
2827
- result = await BaseFileManager.downloadAllFiles({
2828
- transactionIds: transactionIdsToDownload,
2829
- arweaveHost: BaseArweaveClient.getBaseUrl(),
2830
- arweaveBaseUrls,
2831
- excludedTransactions,
2832
- expectedContentHashes,
2833
- compositeChildPaths,
2834
- skipExisting: options?.skipExisting,
2835
- });
2836
- }
2837
- if (result) {
2838
- for (const transactionId of result.hashMismatchTransactionIds) {
2839
- // Not saved by the worker; left for a later run (a gateway may serve the right bytes then).
2840
- logger$3(`[download] ${transactionId}: downloaded bytes do not match Content-SHA-256; skipped`);
2841
- }
2842
- }
2843
- return true;
2844
- };
2845
- const runBulkDownloadWithDedupe = async (transactionIds) => {
2846
- if (bulkDownloadInFlight) {
2847
- return bulkDownloadInFlight;
2848
- }
2849
- bulkDownloadInFlight = (async () => {
2850
- try {
2851
- await downloadTransactionIds(transactionIds, { skipExisting: true });
2852
- }
2853
- finally {
2854
- bulkDownloadInFlight = null;
2855
- }
2856
- })();
2857
- return bulkDownloadInFlight;
2858
- };
2859
- const downloadAllFilesBinaryRequestHandler = async (addressesHint) => {
2860
- if (!supportsOpfsFileDownloads()) {
2861
- return;
2862
- }
2863
- // Waits for init to open the database, if it hasn't yet
2864
- const fromDb = await getAllAddressesFromDb();
2865
- const addresses = mergeAddressLists(addressesHint, fromDb);
2866
- if (!addresses || addresses.length === 0) {
2867
- return;
2868
- }
2869
- const queryClient = BaseQueryClient.getQueryClient();
2870
- const easClient = BaseEasClient.getEasClient();
2871
- const filesMetadataQueryKey = ['getFilesMetadata', ...addresses];
2872
- const fetchFilesMetadata = () => easClient.request(GET_FILES_METADATA, {
2873
- where: {
2874
- attester: {
2875
- in: addresses,
2876
- },
2877
- schema: {
2878
- is: {
2879
- id: {
2880
- equals: '0x55fdefb36fcbbaebeb7d6b41dc3a1a9666e4e42154267c889de064faa7ede517',
2881
- },
2882
- },
2883
- },
2884
- },
2885
- });
2886
- let { filesMetadata } = await queryClient.fetchQuery({
2887
- queryKey: filesMetadataQueryKey,
2888
- queryFn: fetchFilesMetadata,
2889
- staleTime: GET_FILES_METADATA_STALE_MS,
2890
- });
2891
- if ((!Array.isArray(filesMetadata) || filesMetadata.length === 0) && addresses.length > 0) {
2892
- await queryClient.removeQueries({ queryKey: filesMetadataQueryKey });
2893
- ({ filesMetadata } = await queryClient.fetchQuery({
2894
- queryKey: filesMetadataQueryKey,
2895
- queryFn: fetchFilesMetadata,
2896
- staleTime: GET_FILES_METADATA_STALE_MS,
2897
- }));
2898
- }
2899
- const filesRoot = BaseFileManager.getWorkingDir();
2900
- await BaseFileManager.createDirIfNotExists(filesRoot);
2901
- await BaseFileManager.createDirIfNotExists(BaseFileManager.getFilesPath('html'));
2902
- await BaseFileManager.createDirIfNotExists(BaseFileManager.getFilesPath('json'));
2903
- await BaseFileManager.createDirIfNotExists(BaseFileManager.getFilesPath('images'));
2904
- await BaseFileManager.createDirIfNotExists(BaseFileManager.getFilesPath('files'));
2905
- const excludedTransactions = await getExcludedTransactions();
2906
- const fromEas = parseTransactionIdsFromMetadata(filesMetadata ?? [], excludedTransactions);
2907
- const fromLocal = await collectTransactionIdsFromLocalMetadata();
2908
- const transactionIds = Array.from(new Set([...fromEas, ...fromLocal]));
2909
- await runBulkDownloadWithDedupe(transactionIds);
2910
- };
2911
- const downloadTransactionIdWithDedupe = async (transactionId) => {
2912
- if (!supportsOpfsFileDownloads() || !transactionId || transactionId.trim().length === 0) {
2913
- return false;
2914
- }
2915
- const normalizedTxId = normalizeArweaveTxId(transactionId);
2916
- if (!normalizedTxId) {
2917
- logger$3(`[download] not an Arweave transaction id: ${transactionId}`);
2918
- return false;
2919
- }
2920
- const existing = lazyDownloadInFlightByTransactionId.get(normalizedTxId);
2921
- if (existing) {
2922
- return existing;
2923
- }
2924
- const inFlight = (async () => {
2925
- try {
2926
- // No skipExisting: the caller found no local file, so a file on disk it could not see is
2927
- // written again and announced.
2928
- const ok = await downloadTransactionIds([normalizedTxId]);
2929
- return ok;
2930
- }
2931
- finally {
2932
- lazyDownloadInFlightByTransactionId.delete(normalizedTxId);
2933
- }
2934
- })();
2935
- lazyDownloadInFlightByTransactionId.set(normalizedTxId, inFlight);
2936
- return inFlight;
2937
- };
2938
- const scheduleBulkFilesDownloadFromEasSyncThrottled = throttle((addressesHint) => {
2939
- void downloadAllFilesBinaryRequestHandler(addressesHint).catch((error) => {
2940
- logger$3('[scheduleBulkFilesDownloadFromEasSync] failed', error);
2941
- });
2942
- }, AUTO_BULK_DOWNLOAD_THROTTLE_MS, { leading: true, trailing: false });
2943
- const scheduleBulkFilesDownloadFromEasSync = (addressesHint) => {
2944
- if (!supportsOpfsFileDownloads()) {
2945
- return;
2946
- }
2947
- scheduleBulkFilesDownloadFromEasSyncThrottled(addressesHint);
2948
- };
2949
-
2950
- var download = /*#__PURE__*/Object.freeze({
2951
- __proto__: null,
2952
- downloadAllFilesBinaryRequestHandler: downloadAllFilesBinaryRequestHandler,
2953
- downloadTransactionIdWithDedupe: downloadTransactionIdWithDedupe,
2954
- scheduleBulkFilesDownloadFromEasSync: scheduleBulkFilesDownloadFromEasSync
2955
- });
2956
-
2957
- const isFile = async (path) => {
2958
- try {
2959
- const fs = await BaseFileManager.getFs();
2960
- return (await fs.promises.stat(path)).isFile();
2961
- }
2962
- catch {
2963
- return false;
2964
- }
2965
- };
2966
- /**
2967
- * A file in `subdir` (relative to the files root) for a stored reference: the file of that exact
2968
- * name, else the first whose name contains one of `needles` (a downloaded file is named after its
2969
- * transaction id, sometimes with an extension). Folders (an image's size variants) don't count.
2970
- */
2971
- async function findFileInDir(subdir, needles) {
2972
- const wanted = needles.filter((n) => !!n);
2973
- if (wanted.length === 0)
2974
- return undefined;
2975
- for (const needle of wanted) {
2976
- const exact = BaseFileManager.getFilesPath(subdir, needle);
2977
- if ((await BaseFileManager.pathExists(exact)) && (await isFile(exact)))
2978
- return exact;
2979
- }
2980
- const dirPath = BaseFileManager.getFilesPath(subdir);
2981
- if (!(await BaseFileManager.pathExists(dirPath)))
2982
- return undefined;
2983
- const fs = await BaseFileManager.getFs();
2984
- const path = BaseFileManager.getPathModule();
2985
- const names = await fs.promises.readdir(dirPath);
2986
- for (const needle of wanted) {
2987
- for (const name of names) {
2988
- if (!path.basename(name).includes(needle))
2989
- continue;
2990
- const candidate = BaseFileManager.getFilesPath(subdir, name);
2991
- if (await isFile(candidate))
2992
- return candidate;
2993
- }
2994
- }
2995
- return undefined;
2996
- }
2997
-
2998
- var findFile = /*#__PURE__*/Object.freeze({
2999
- __proto__: null,
3000
- findFileInDir: findFileInDir
3001
- });
3002
-
3003
- const logger$2 = debug('seedSdk:helpers:ensureImageLocal');
3004
- /** Arweave transaction ids are 43 chars in the URL-safe base64 alphabet. */
3005
- const ARWEAVE_TX_ID_RE = /^[a-z0-9_-]{43}$/i;
3006
- const IMAGE_SIZES = [
3007
- ImageSize.EXTRA_SMALL,
3008
- ImageSize.SMALL,
3009
- ImageSize.MEDIUM,
3010
- ImageSize.LARGE,
3011
- ImageSize.EXTRA_LARGE,
3012
- ];
3013
- const inFlightByKey = new Map();
3014
- const normalizeArweaveTxIdForEnsure = (raw) => {
3015
- if (raw == null || raw === '')
3016
- return undefined;
3017
- const t = String(raw).trim();
3018
- if (ARWEAVE_TX_ID_RE.test(t))
3019
- return t;
3020
- const m = t.match(/^([a-z0-9_-]{43})(?:\.[^/]+)?$/i);
3021
- if (m)
3022
- return m[1];
3023
- const embedded = t.match(/[a-z0-9_-]{43}/gi);
3024
- if (embedded) {
3025
- for (const candidate of embedded) {
3026
- if (ARWEAVE_TX_ID_RE.test(candidate))
3027
- return candidate;
3028
- }
3029
- }
3030
- return undefined;
3031
- };
3032
- const closestImageSize = (width) => {
3033
- let best = ImageSize.EXTRA_SMALL;
3034
- let bestDist = Math.abs(best - width);
3035
- for (const size of IMAGE_SIZES) {
3036
- const dist = Math.abs(size - width);
3037
- if (dist < bestDist) {
3038
- best = size;
3039
- bestDist = dist;
3040
- }
3041
- }
3042
- return best;
3043
- };
3044
- const mapRequestedWidthsToImageSizes = (widths) => {
3045
- const requested = widths && widths.length > 0 ? widths : [ImageSize.EXTRA_SMALL];
3046
- const unique = new Set();
3047
- for (const w of requested) {
3048
- if (typeof w !== 'number' || !Number.isFinite(w) || w <= 0)
3049
- continue;
3050
- unique.add(closestImageSize(w));
3051
- }
3052
- if (unique.size === 0) {
3053
- unique.add(ImageSize.EXTRA_SMALL);
3054
- }
3055
- return Array.from(unique).sort((a, b) => a - b);
3056
- };
3057
- const getBasename = (filePath) => {
3058
- const path = BaseFileManager.getPathModule();
3059
- return path.basename(filePath);
3060
- };
3061
- const getBasenameWithoutExtension = (fileName) => {
3062
- const match = fileName.match(/^(.*[\/\\])?([^\/\\]+?)(\.[^.\/\\]*)?$/);
3063
- return match?.[2] ?? fileName;
3064
- };
3065
- const sizedVariantPath = (originalFilePath, size) => {
3066
- const path = BaseFileManager.getPathModule();
3067
- const dir = path.dirname(originalFilePath);
3068
- const base = getBasenameWithoutExtension(getBasename(originalFilePath));
3069
- return path.join(dir, String(size), `${base}.webp`);
3070
- };
3071
- const ensureKey = (txId, fileName, sizes) => {
3072
- const idPart = txId ?? fileName ?? '';
3073
- return `${idPart}|${sizes.join(',')}`;
3074
- };
3075
- const runEnsureImageLocal = async (params) => {
3076
- if (!supportsOpfsFileDownloads()) {
3077
- return { status: 'unsupported' };
3078
- }
3079
- const fileName = params.fileName?.trim() || undefined;
3080
- const txId = normalizeArweaveTxIdForEnsure(params.transactionId) ??
3081
- normalizeArweaveTxIdForEnsure(fileName);
3082
- if (!txId && !fileName) {
3083
- return { status: 'missing-id' };
3084
- }
3085
- const targetSizes = mapRequestedWidthsToImageSizes(params.widths);
3086
- const needles = [fileName, txId].filter((n) => typeof n === 'string' && n.length > 0);
3087
- try {
3088
- let filePath = await findFileInDir('images', needles);
3089
- if (!filePath && txId) {
3090
- await downloadTransactionIdWithDedupe(txId);
3091
- filePath = await findFileInDir('images', needles);
3092
- }
3093
- if (!filePath) {
3094
- return { status: 'failed' };
3095
- }
3096
- const createdWidths = [];
3097
- for (const size of targetSizes) {
3098
- const variantPath = sizedVariantPath(filePath, size);
3099
- const exists = await BaseFileManager.pathExists(variantPath);
3100
- if (exists)
3101
- continue;
3102
- await BaseFileManager.resizeImage({
3103
- filePath,
3104
- width: size,
3105
- height: size,
3106
- });
3107
- createdWidths.push(size);
3108
- }
3109
- return {
3110
- status: 'ready',
3111
- filePath,
3112
- createdWidths,
3113
- };
3114
- }
3115
- catch (error) {
3116
- logger$2('ensureImageLocal failed', error);
3117
- return { status: 'failed' };
3118
- }
3119
- };
3120
- /**
3121
- * Ensure an image original exists in OPFS (downloading from Arweave if needed)
3122
- * and create only the requested ImageSize variant(s) for that one file.
3123
- */
3124
- const ensureImageLocal = async (params) => {
3125
- if (!supportsOpfsFileDownloads()) {
3126
- return { status: 'unsupported' };
3127
- }
3128
- const fileName = params.fileName?.trim() || undefined;
3129
- const txId = normalizeArweaveTxIdForEnsure(params.transactionId) ??
3130
- normalizeArweaveTxIdForEnsure(fileName);
3131
- if (!txId && !fileName) {
3132
- return { status: 'missing-id' };
3133
- }
3134
- const targetSizes = mapRequestedWidthsToImageSizes(params.widths);
3135
- const key = ensureKey(txId, fileName, targetSizes);
3136
- const existing = inFlightByKey.get(key);
3137
- if (existing) {
3138
- return existing;
3139
- }
3140
- const inFlight = (async () => {
3141
- try {
3142
- return await runEnsureImageLocal({
3143
- transactionId: txId,
3144
- fileName,
3145
- widths: targetSizes,
3146
- });
3147
- }
3148
- finally {
3149
- inFlightByKey.delete(key);
3150
- }
3151
- })();
3152
- inFlightByKey.set(key, inFlight);
3153
- return inFlight;
3154
- };
3155
-
3156
- var ensureImageLocal$1 = /*#__PURE__*/Object.freeze({
3157
- __proto__: null,
3158
- closestImageSize: closestImageSize,
3159
- ensureImageLocal: ensureImageLocal,
3160
- normalizeArweaveTxIdForEnsure: normalizeArweaveTxIdForEnsure
3161
- });
3162
-
3163
- /**
3164
- * File extension <-> MIME type, shared by the upload side (`getPublishUploads.ts`, which sets the
3165
- * `Content-Type` tag) and the download worker (`browser/workers/filesDownload.ts`, which picks the
3166
- * extension of a file saved under `files/`). The worker runs from a string and cannot import this
3167
- * module, so {@link MIME_TO_EXTENSION} is injected into its source as JSON.
3168
- */
3169
- /**
3170
- * Lowercase extension (no dot) to MIME type. Includes the odd extensions the SDK itself produces:
3171
- * `saveImage` names a file `<id>.<subtype>` (e.g. `.svg+xml`, `.x-icon`), so subtypes appear here too.
3172
- */
3173
- const EXTENSION_TO_MIME = {
3174
- // images
3175
- png: 'image/png',
3176
- apng: 'image/apng',
3177
- jpg: 'image/jpeg',
3178
- jpeg: 'image/jpeg',
3179
- jfif: 'image/jpeg',
3180
- gif: 'image/gif',
3181
- webp: 'image/webp',
3182
- avif: 'image/avif',
3183
- heic: 'image/heic',
3184
- heif: 'image/heif',
3185
- svg: 'image/svg+xml',
3186
- 'svg+xml': 'image/svg+xml',
3187
- bmp: 'image/bmp',
3188
- ico: 'image/x-icon',
3189
- 'x-icon': 'image/x-icon',
3190
- 'vnd.microsoft.icon': 'image/x-icon',
3191
- tif: 'image/tiff',
3192
- tiff: 'image/tiff',
3193
- // text / documents
3194
- html: 'text/html',
3195
- htm: 'text/html',
3196
- xhtml: 'application/xhtml+xml',
3197
- json: 'application/json',
3198
- txt: 'text/plain',
3199
- plain: 'text/plain',
3200
- md: 'text/markdown',
3201
- markdown: 'text/markdown',
3202
- csv: 'text/csv',
3203
- tsv: 'text/tab-separated-values',
3204
- xml: 'application/xml',
3205
- css: 'text/css',
3206
- js: 'text/javascript',
3207
- mjs: 'text/javascript',
3208
- pdf: 'application/pdf',
3209
- rtf: 'application/rtf',
3210
- epub: 'application/epub+zip',
3211
- zip: 'application/zip',
3212
- gz: 'application/gzip',
3213
- doc: 'application/msword',
3214
- docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
3215
- xls: 'application/vnd.ms-excel',
3216
- xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
3217
- ppt: 'application/vnd.ms-powerpoint',
3218
- pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
3219
- odt: 'application/vnd.oasis.opendocument.text',
3220
- // audio
3221
- mp3: 'audio/mpeg',
3222
- mpeg: 'audio/mpeg',
3223
- wav: 'audio/wav',
3224
- ogg: 'audio/ogg',
3225
- oga: 'audio/ogg',
3226
- opus: 'audio/opus',
3227
- m4a: 'audio/mp4',
3228
- aac: 'audio/aac',
3229
- flac: 'audio/flac',
3230
- weba: 'audio/webm',
3231
- // video
3232
- mp4: 'video/mp4',
3233
- m4v: 'video/mp4',
3234
- webm: 'video/webm',
3235
- mov: 'video/quicktime',
3236
- quicktime: 'video/quicktime',
3237
- ogv: 'video/ogg',
3238
- mkv: 'video/x-matroska',
3239
- avi: 'video/x-msvideo',
3240
- // fonts
3241
- woff: 'font/woff',
3242
- woff2: 'font/woff2',
3243
- ttf: 'font/ttf',
3244
- otf: 'font/otf',
3245
- };
3246
- /**
3247
- * MIME type to the extension a downloaded file gets. One canonical extension per type (the
3248
- * first one listed here wins over {@link EXTENSION_TO_MIME}'s aliases), plus a few common
3249
- * non-canonical spellings a gateway may send.
3250
- */
3251
- const MIME_TO_EXTENSION = (() => {
3252
- const preferred = {
3253
- 'image/jpeg': 'jpg',
3254
- 'image/svg+xml': 'svg',
3255
- 'image/x-icon': 'ico',
3256
- 'image/tiff': 'tiff',
3257
- 'text/html': 'html',
3258
- 'text/plain': 'txt',
3259
- 'text/markdown': 'md',
3260
- 'text/javascript': 'js',
3261
- 'audio/mpeg': 'mp3',
3262
- 'audio/ogg': 'ogg',
3263
- 'audio/mp4': 'm4a',
3264
- 'video/mp4': 'mp4',
3265
- 'video/quicktime': 'mov',
3266
- 'video/ogg': 'ogv',
3267
- };
3268
- const out = { ...preferred };
3269
- for (const [ext, mime] of Object.entries(EXTENSION_TO_MIME)) {
3270
- if (!(mime in out))
3271
- out[mime] = ext;
3272
- }
3273
- // Non-canonical names some servers use for the same formats.
3274
- Object.assign(out, {
3275
- 'audio/mp3': 'mp3',
3276
- 'audio/x-wav': 'wav',
3277
- 'audio/wave': 'wav',
3278
- 'audio/vnd.wave': 'wav',
3279
- 'audio/x-m4a': 'm4a',
3280
- 'audio/x-flac': 'flac',
3281
- 'application/x-zip-compressed': 'zip',
3282
- 'image/vnd.microsoft.icon': 'ico',
3283
- });
3284
- return out;
3285
- })();
3286
- /**
3287
- * Content types that say nothing about the content. A Blob carrying one of these is treated as
3288
- * untyped.
3289
- */
3290
- const GENERIC_MIME_TYPES = [
3291
- '',
3292
- 'application/octet-stream',
3293
- 'binary/octet-stream',
3294
- ];
3295
- /** Lowercase `type/subtype` without parameters; `undefined` when empty or generic. */
3296
- const normalizeMimeType = (mimeType) => {
3297
- const t = (mimeType ?? '').split(';')[0].trim().toLowerCase();
3298
- if (!t || GENERIC_MIME_TYPES.includes(t) || !t.includes('/'))
3299
- return undefined;
3300
- return t;
3301
- };
3302
- /** MIME type for a path or file name by its extension, if it has a known one. */
3303
- const getMimeTypeFromPath = (filePathOrName) => {
3304
- const name = filePathOrName.split('/').pop() ?? '';
3305
- const dot = name.lastIndexOf('.');
3306
- if (dot <= 0 || dot === name.length - 1)
3307
- return undefined;
3308
- return EXTENSION_TO_MIME[name.slice(dot + 1).toLowerCase()];
3309
- };
3310
- /** Extension (no dot) for a MIME type, if it has a well-known one. */
3311
- const getExtensionForMimeType = (mimeType) => {
3312
- const t = normalizeMimeType(mimeType);
3313
- return t ? MIME_TO_EXTENSION[t] : undefined;
3314
- };
3315
- /**
3316
- * MIME type from the leading bytes, for formats with an unambiguous signature. Used when a local
3317
- * file has no (known) extension, e.g. a file seed named after its id. Self-contained: the download
3318
- * and resize workers get it as source text.
3319
- */
3320
- const sniffMimeType = (bytes) => {
3321
- const at = (offset, ...sig) => bytes.length >= offset + sig.length && sig.every((b, i) => bytes[offset + i] === b);
3322
- const ascii = (offset, s) => at(offset, ...Array.from(s).map((c) => c.charCodeAt(0)));
3323
- if (at(0, 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a))
3324
- return 'image/png';
3325
- if (at(0, 0xff, 0xd8, 0xff))
3326
- return 'image/jpeg';
3327
- if (ascii(0, 'GIF87a') || ascii(0, 'GIF89a'))
3328
- return 'image/gif';
3329
- if (ascii(0, 'RIFF') && ascii(8, 'WEBP'))
3330
- return 'image/webp';
3331
- if (ascii(0, 'RIFF') && ascii(8, 'WAVE'))
3332
- return 'audio/wav';
3333
- if (ascii(0, 'RIFF') && ascii(8, 'AVI '))
3334
- return 'video/x-msvideo';
3335
- if (ascii(0, 'BM') && bytes.length > 14)
3336
- return 'image/bmp';
3337
- if (at(0, 0x49, 0x49, 0x2a, 0x00) || at(0, 0x4d, 0x4d, 0x00, 0x2a))
3338
- return 'image/tiff';
3339
- if (ascii(0, '%PDF-'))
3340
- return 'application/pdf';
3341
- if (ascii(0, 'OggS'))
3342
- return 'audio/ogg';
3343
- if (ascii(0, 'fLaC'))
3344
- return 'audio/flac';
3345
- if (ascii(0, 'ID3'))
3346
- return 'audio/mpeg';
3347
- if (at(0, 0x1a, 0x45, 0xdf, 0xa3))
3348
- return 'video/webm';
3349
- if (ascii(4, 'ftyp')) {
3350
- const brand = String.fromCharCode(...Array.from(bytes.slice(8, 12)));
3351
- if (brand === 'avif' || brand === 'avis')
3352
- return 'image/avif';
3353
- if (['heic', 'heix', 'heif', 'mif1', 'msf1'].includes(brand))
3354
- return 'image/heic';
3355
- if (brand === 'qt ')
3356
- return 'video/quicktime';
3357
- if (brand === 'M4A ')
3358
- return 'audio/mp4';
3359
- return 'video/mp4';
3360
- }
3361
- if (at(0, 0x00, 0x00, 0x01, 0x00))
3362
- return 'image/x-icon';
3363
- return undefined;
3364
- };
3365
- /**
3366
- * Best MIME type for upload bytes: the source's own type (a Blob/File `type`) when it is specific,
3367
- * then the file name's extension, then the bytes' signature.
3368
- */
3369
- const resolveUploadMimeType = (params) => normalizeMimeType(params.sourceType) ??
3370
- (params.fileName ? getMimeTypeFromPath(params.fileName) : undefined) ??
3371
- (params.bytes ? sniffMimeType(params.bytes) : undefined);
3372
-
3373
- class SeedEventEmitter {
3374
- constructor() {
3375
- this.listeners = new Map();
3376
- }
3377
- on(event, listener) {
3378
- const set = this.listeners.get(event) ?? new Set();
3379
- set.add(listener);
3380
- this.listeners.set(event, set);
3381
- return this;
3382
- }
3383
- addListener(event, listener) {
3384
- return this.on(event, listener);
3385
- }
3386
- once(event, listener) {
3387
- const wrapped = (...args) => {
3388
- this.off(event, wrapped);
3389
- listener(...args);
3390
- };
3391
- return this.on(event, wrapped);
3392
- }
3393
- off(event, listener) {
3394
- const set = this.listeners.get(event);
3395
- if (!set)
3396
- return this;
3397
- set.delete(listener);
3398
- if (set.size === 0)
3399
- this.listeners.delete(event);
3400
- return this;
3401
- }
3402
- removeListener(event, listener) {
3403
- return this.off(event, listener);
3404
- }
3405
- removeAllListeners(event) {
3406
- if (typeof event === 'undefined') {
3407
- this.listeners.clear();
3408
- return this;
3409
- }
3410
- this.listeners.delete(event);
3411
- return this;
3412
- }
3413
- /**
3414
- * Calls every listener. A listener that throws is logged and skipped: the others still
3415
- * run, and the error never reaches the SDK code that emitted the event (which may be
3416
- * midway through a save).
3417
- */
3418
- emit(event, ...args) {
3419
- const set = this.listeners.get(event);
3420
- if (!set || set.size === 0)
3421
- return false;
3422
- for (const listener of [...set]) {
3423
- try {
3424
- listener(...args);
3425
- }
3426
- catch (error) {
3427
- console.error(`[seed] listener for "${String(event)}" threw:`, error);
3428
- }
3429
- }
3430
- return true;
3431
- }
3432
- }
3433
- const eventEmitter = new SeedEventEmitter();
3434
-
3435
- const logger$1 = debug('seedSdk:browser:db:changeFeed');
3436
- /** Log rows older than this are pruned by the leader tab. */
3437
- const ENTITY_CHANGES_RETENTION_MS = 10 * 60000;
3438
- /** The leader prunes at most this often. */
3439
- const ENTITY_CHANGES_PRUNE_EVERY_MS = 60000;
3440
- const byLocalId = new Map();
3441
- const byUid = new Map();
3442
- const registrations = new Set();
3443
- let feed;
3444
- let cursor = 0;
3445
- let reading;
3446
- let readAgain = false;
3447
- let lastPrunedAt = 0;
3448
- const addTo = (map, key, registration) => {
3449
- let set = map.get(key);
3450
- if (!set)
3451
- map.set(key, (set = new Set()));
3452
- set.add(registration);
3453
- };
3454
- const removeFrom = (map, key, registration) => {
3455
- const set = map.get(key);
3456
- if (!set)
3457
- return;
3458
- set.delete(registration);
3459
- if (set.size === 0)
3460
- map.delete(key);
3461
- };
3462
- /**
3463
- * Calls `listener` after a committed write to one of `watch.tables` for the watched seed, matched
3464
- * by local id or (case-insensitively) by uid. A read of the log that has several matching rows
3465
- * calls it once. When the tab missed rows (its cursor is older than the oldest kept row), every
3466
- * listener is called once. Returns the unregister function.
3467
- */
3468
- function watchEntityChanges(watch, listener) {
3469
- const registration = {
3470
- watch,
3471
- tables: new Set(watch.tables),
3472
- propertyNames: watch.propertyNames ? new Set(watch.propertyNames) : undefined,
3473
- listener,
3474
- };
3475
- const localId = watch.seedLocalId || undefined;
3476
- const uid = watch.seedUid ? watch.seedUid.toLowerCase() : undefined;
3477
- registrations.add(registration);
3478
- if (localId)
3479
- addTo(byLocalId, localId, registration);
3480
- if (uid)
3481
- addTo(byUid, uid, registration);
3482
- return () => {
3483
- registrations.delete(registration);
3484
- if (localId)
3485
- removeFrom(byLocalId, localId, registration);
3486
- if (uid)
3487
- removeFrom(byUid, uid, registration);
3488
- };
3489
- }
3490
- /**
3491
- * Watches `watch` and runs `read` once per dispatch, handing its result to `onRows` when it differs
3492
- * from the last one handed over. A read that finishes after a newer one started is dropped, so an
3493
- * older result never replaces a newer one. Pass `immediate` to also run it once right away.
3494
- */
3495
- function watchEntityRows(watch, read, onRows, options = {}) {
3496
- let latest = 0;
3497
- let closed = false;
3498
- let lastJson;
3499
- const run = () => {
3500
- const token = ++latest;
3501
- read().then((rows) => {
3502
- if (closed || token !== latest)
3503
- return;
3504
- // A write that left these rows as they were (this entity's own write, read back) doesn't
3505
- // reach the handler again.
3506
- const json = JSON.stringify(rows);
3507
- if (json === lastJson)
3508
- return;
3509
- lastJson = json;
3510
- onRows(rows);
3511
- }, (error) => {
3512
- if (closed || token !== latest)
3513
- return;
3514
- if (options.onError)
3515
- options.onError(error);
3516
- else
3517
- logger$1('entity read failed', error);
3518
- });
3519
- };
3520
- const unregister = watchEntityChanges(watch, run);
3521
- if (options.immediate)
3522
- run();
3523
- return {
3524
- unsubscribe: () => {
3525
- closed = true;
3526
- unregister();
3527
- },
3528
- };
3529
- }
3530
- /** Whether this tab's feed runs (a browser database is open); Node has none. */
3531
- function isChangeFeedRunning() {
3532
- return feed !== undefined;
3533
- }
3534
- const callAll = (targets) => {
3535
- for (const registration of targets) {
3536
- try {
3537
- registration.listener();
3538
- }
3539
- catch (error) {
3540
- logger$1('listener threw', error);
3541
- }
3542
- }
3543
- };
3544
- const dispatch = (rows) => {
3545
- const targets = new Set();
3546
- for (const row of rows) {
3547
- const candidates = [
3548
- ...(row.seedLocalId ? byLocalId.get(row.seedLocalId) ?? [] : []),
3549
- ...(row.seedUid ? byUid.get(row.seedUid.toLowerCase()) ?? [] : []),
3550
- ];
3551
- for (const registration of candidates) {
3552
- if (!registration.tables.has(row.tbl))
3553
- continue;
3554
- if (row.tbl === 'metadata' &&
3555
- registration.propertyNames &&
3556
- row.propertyName != null &&
3557
- !registration.propertyNames.has(row.propertyName)) {
3558
- continue;
3559
- }
3560
- targets.add(registration);
3561
- }
3562
- }
3563
- callAll(targets);
3564
- };
3565
- const readNewRows = async (db) => {
3566
- const rows = await db
3567
- .select({
3568
- seq: entityChanges.seq,
3569
- tbl: entityChanges.tbl,
3570
- seedLocalId: entityChanges.seedLocalId,
3571
- seedUid: entityChanges.seedUid,
3572
- propertyName: entityChanges.propertyName,
3573
- })
3574
- .from(entityChanges)
3575
- .where(gt(entityChanges.seq, cursor))
3576
- .orderBy(entityChanges.seq);
3577
- if (feed?.db !== db || rows.length === 0)
3578
- return;
3579
- // Writers are serialized, so committed seqs have no holes: a missing first row was pruned.
3580
- const missed = rows[0].seq > cursor + 1;
3581
- cursor = rows[rows.length - 1].seq;
3582
- if (missed) {
3583
- logger$1(`cursor fell behind the log; notifying all ${registrations.size} watches`);
3584
- callAll([...registrations]);
3585
- }
3586
- else {
3587
- dispatch(rows);
3588
- }
3589
- await maybePrune(db);
3590
- };
3591
- const maybePrune = async (db) => {
3592
- const now = Date.now();
3593
- if (!isLeaderTab() || now - lastPrunedAt < ENTITY_CHANGES_PRUNE_EVERY_MS)
3594
- return;
3595
- lastPrunedAt = now;
3596
- try {
3597
- await pruneEntityChanges(db, now);
3598
- }
3599
- catch (error) {
3600
- logger$1('prune failed', error);
3601
- }
3602
- };
3603
- /** Deletes log rows older than ENTITY_CHANGES_RETENTION_MS. Deleting doesn't raise max(seq), so it dispatches nothing. */
3604
- async function pruneEntityChanges(db, now = Date.now()) {
3605
- await db.delete(entityChanges).where(lt(entityChanges.at, now - ENTITY_CHANGES_RETENTION_MS));
3606
- }
3607
- /** Reads once at a time; an emission during a read makes it read once more. */
3608
- const scheduleRead = (db) => {
3609
- if (reading) {
3610
- readAgain = true;
3611
- return;
3612
- }
3613
- reading = (async () => {
3614
- try {
3615
- do {
3616
- readAgain = false;
3617
- await readNewRows(db);
3618
- } while (readAgain && feed?.db === db);
3619
- }
3620
- catch (error) {
3621
- logger$1('read failed', error);
3622
- }
3623
- finally {
3624
- reading = undefined;
3625
- }
3626
- })();
3627
- };
3628
- /**
3629
- * Starts this tab's feed on `db` (once its migrations ran), at the newest seq allocated so far:
3630
- * entities do their own initial read. A second call replaces the first feed.
3631
- */
3632
- async function startChangeFeed(db, { liveQuery }) {
3633
- stopChangeFeed();
3634
- // sqlite_sequence keeps the last seq even when pruning emptied the table.
3635
- const [row] = await db.all(sql `SELECT seq FROM sqlite_sequence WHERE name = 'entity_changes'`);
3636
- cursor = Number(row?.seq ?? 0);
3637
- const subscription = liveQuery((tag) => tag `SELECT max(seq) AS seq FROM entity_changes`).subscribe({
3638
- next: ([latest]) => {
3639
- if (latest?.seq != null && Number(latest.seq) > cursor)
3640
- scheduleRead(db);
3641
- },
3642
- error: (error) => logger$1('feed query failed', error),
3643
- });
3644
- feed = { db, unsubscribe: () => subscription.unsubscribe() };
3645
- logger$1(`started at seq ${cursor}`);
3646
- }
3647
- function stopChangeFeed() {
3648
- feed?.unsubscribe();
3649
- feed = undefined;
3650
- readAgain = false;
3651
- }
3652
-
3653
- /**
3654
- * Blob URLs the SDK mints for property values (File and Image display URLs). Only these are revoked
3655
- * by `ItemProperty`: a blob URL the app passed in as a value stays the app's to revoke.
3656
- */
3657
- const minted = new Set();
3658
- const createSessionObjectUrl = (blob) => {
3659
- const url = URL.createObjectURL(blob);
3660
- minted.add(url);
3661
- return url;
3662
- };
3663
- const isSessionObjectUrl = (value) => typeof value === 'string' && minted.has(value);
3664
- const revokeSessionObjectUrl = (url) => {
3665
- if (!minted.delete(url))
3666
- return;
3667
- URL.revokeObjectURL(url);
3668
- };
3669
-
3670
- /** Emitted by the client manager after `app_state.addresses` is written (post-`setAddresses`). */
3671
- const ADDRESSES_PERSISTED_EVENT = 'addresses.persisted';
3672
- /** Emitted after hard-deleting local on-chain seed copies for publisher addresses. */
3673
- const LOCAL_COPIES_REMOVED_EVENT = 'localCopies.removed';
3674
- function parseAddressesPersistedPayload(value) {
3675
- if (!value || typeof value !== 'object') {
3676
- return { owned: [], watched: [] };
3677
- }
3678
- const v = value;
3679
- const owned = Array.isArray(v.owned)
3680
- ? v.owned.filter((a) => typeof a === 'string')
3681
- : [];
3682
- const watched = Array.isArray(v.watched)
3683
- ? v.watched.filter((a) => typeof a === 'string')
3684
- : [];
3685
- return { owned, watched };
3686
- }
3687
-
3688
- const logger = debug('seedSdk:helpers:tabEvents');
3689
- /**
3690
- * Keeps other tabs current (docs/MULTI_TAB.md). The event bus is per tab, so events that mean "the
3691
- * shared database or OPFS changed" are also posted to the other tabs, which refresh their caches
3692
- * and emit the same event locally. Events are posted only where they originate, so a relayed event
3693
- * is never relayed again.
3694
- */
3695
- /** Emitted with the changed paths when files change outside this tab's file-system cache. */
3696
- const FILES_CHANGED_EVENT = 'fs.files.changed';
3697
- /** Emits on this tab's event bus and in every other tab of this database. */
3698
- function emitAcrossTabs(name, payload) {
3699
- if (payload === undefined) {
3700
- eventEmitter.emit(name);
3701
- }
3702
- else {
3703
- eventEmitter.emit(name, payload);
3704
- }
3705
- postTabMessage({ type: 'event', name, payload });
3706
- }
3707
- /**
3708
- * A file saved through this tab's file system: emits `file-saved` here. Other tabs hear about it
3709
- * from the file system itself (see queueFilesChangedNotice), once the write has reached OPFS.
3710
- */
3711
- function notifyFileSaved(filePath) {
3712
- eventEmitter.emit('file-saved', filePath);
3713
- }
3714
- /**
3715
- * Files written straight to OPFS by a worker: refreshes this tab's file-system cache, emits
3716
- * FILES_CHANGED_EVENT, and tells other tabs to do the same.
3717
- */
3718
- async function notifyFilesWrittenOutsideCache(paths) {
3719
- if (paths.length === 0)
3720
- return;
3721
- await BaseFileManager.invalidateCachedPaths(paths);
3722
- eventEmitter.emit(FILES_CHANGED_EVENT, paths);
3723
- queueFilesChangedNotice(paths);
3724
- }
3725
- /**
3726
- * How long changed paths gather before one `files-changed` message goes out. Counted from the
3727
- * first queued path, so a steady stream of writes still sends once per window, never later.
3728
- */
3729
- const FILES_CHANGED_FLUSH_MS = 25;
3730
- /** Keeps a message for a huge batch (a bulk download) from being one giant structured clone. */
3731
- const FILES_CHANGED_MAX_PATHS = 500;
3732
- let pendingChangedPaths = new Set();
3733
- let flushTimer;
3734
- let flushOnPageHide = false;
3735
- /**
3736
- * Tells other tabs that these absolute ZenFS paths changed in OPFS, so they drop and re-read their
3737
- * cached copies (the sync API's in-memory copy in particular). The browser file system calls this
3738
- * for every file it writes, truncates or removes (`TolerantWebAccessFS.onOpfsChange`, wired in
3739
- * `BrowserFileManager.initializeFileSystem`), after the change reached OPFS. Paths are coalesced
3740
- * for FILES_CHANGED_FLUSH_MS, so bulk writes (migrations, schema processing, downloads) send a few
3741
- * messages rather than one per write.
3742
- *
3743
- * Defense in depth only: a tab that misses a message still sees the change on its next async
3744
- * stat() (see TolerantWebAccessFS.stat). A no-op unless this tab coordinates with others.
3745
- */
3746
- function queueFilesChangedNotice(paths) {
3747
- if (!isPostingTabMessages())
3748
- return;
3749
- for (const path of paths)
3750
- pendingChangedPaths.add(path);
3751
- if (flushTimer !== undefined || pendingChangedPaths.size === 0)
3752
- return;
3753
- flushTimer = setTimeout(flushFilesChangedNotices, FILES_CHANGED_FLUSH_MS);
3754
- if (!flushOnPageHide && typeof addEventListener === 'function') {
3755
- flushOnPageHide = true;
3756
- // A tab closing inside the window still sends what it wrote.
3757
- addEventListener('pagehide', flushFilesChangedNotices);
3758
- }
3759
- }
3760
- /** Sends the queued paths now (page hide, tests). */
3761
- function flushFilesChangedNotices() {
3762
- if (flushTimer !== undefined)
3763
- clearTimeout(flushTimer);
3764
- flushTimer = undefined;
3765
- if (pendingChangedPaths.size === 0)
3766
- return;
3767
- const paths = [...pendingChangedPaths];
3768
- pendingChangedPaths = new Set();
3769
- for (let i = 0; i < paths.length; i += FILES_CHANGED_MAX_PATHS) {
3770
- postTabMessage({ type: 'files-changed', paths: paths.slice(i, i + FILES_CHANGED_MAX_PATHS) });
3771
- }
3772
- }
3773
- /** Refreshes this tab's in-memory state for an event another tab emitted, before re-emitting it. */
3774
- async function catchUpOn(name, payload) {
3775
- if (name === LOCAL_COPIES_REMOVED_EVENT) {
3776
- const { removedSeedLocalIds = [], removedSeedUids = [] } = (payload ?? {});
3777
- const _mod_0 = await import('./Item-oPnmIehk.js');
3778
- const _ns_0 = _mod_0.bb;
3779
- const { Item } = _ns_0;
3780
- Item.dropCachedInstancesForSeedIds([...removedSeedLocalIds, ...removedSeedUids]);
3781
- }
3782
- }
3783
- const RELAYED_EVENTS = new Set([
3784
- EAS_SEED_DATA_SYNCED_TO_DB_EVENT,
3785
- ADDRESSES_PERSISTED_EVENT,
3786
- LOCAL_COPIES_REMOVED_EVENT,
3787
- ]);
3788
- let listening = false;
3789
- /** Applies other tabs' events and file changes in this tab. */
3790
- function listenForOtherTabsEvents() {
3791
- if (listening)
3792
- return;
3793
- listening = true;
3794
- onTabMessage((message) => {
3795
- if (message.type === 'event' && RELAYED_EVENTS.has(message.name)) {
3796
- void catchUpOn(message.name, message.payload)
3797
- .catch((error) => logger(`catching up on ${message.name} failed`, error))
3798
- .then(() => {
3799
- if (message.payload === undefined) {
3800
- eventEmitter.emit(message.name);
3801
- }
3802
- else {
3803
- eventEmitter.emit(message.name, message.payload);
3804
- }
3805
- });
3806
- }
3807
- else if (message.type === 'files-changed') {
3808
- // Only refreshes caches: never writes, so never sends files-changed back.
3809
- void BaseFileManager.invalidateCachedPaths(message.paths)
3810
- .catch((error) => logger('refreshing changed files failed', error))
3811
- .then(() => eventEmitter.emit(FILES_CHANGED_EVENT, message.paths));
3812
- }
3813
- });
3814
- }
3815
-
3816
- var tabEvents = /*#__PURE__*/Object.freeze({
3817
- __proto__: null,
3818
- FILES_CHANGED_EVENT: FILES_CHANGED_EVENT,
3819
- FILES_CHANGED_FLUSH_MS: FILES_CHANGED_FLUSH_MS,
3820
- FILES_CHANGED_MAX_PATHS: FILES_CHANGED_MAX_PATHS,
3821
- emitAcrossTabs: emitAcrossTabs,
3822
- flushFilesChangedNotices: flushFilesChangedNotices,
3823
- listenForOtherTabsEvents: listenForOtherTabsEvents,
3824
- notifyFileSaved: notifyFileSaved,
3825
- notifyFilesWrittenOutsideCache: notifyFilesWrittenOutsideCache,
3826
- queueFilesChangedNotice: queueFilesChangedNotice
3827
- });
3828
-
3829
- export { isNode as $, AmbiguousModelError as A, getAllAddressesFromDb as B, loadNameMatchLevels as C, DEFAULT_TEXT_MAX_LENGTH as D, scheduleBulkFilesDownloadFromEasSync as E, isBrowser as F, emitAcrossTabs as G, findSeedRowsByUid as H, INTERNAL_SCHEMA_IDS as I, normalizeHexAddress as J, selectInBatches as K, nameMatchLevels as L, ModelPropertyDataTypes as M, getItemStoragePropertiesForModel as N, resolveItemModelFileId as O, Property as P, EAS_SEED_DATA_SYNCED_TO_DB_EVENT as Q, getExtensionForMimeType as R, SEED_PROTOCOL_SCHEMA_NAME as S, THtmlEmbeddedDataUriPolicy as T, INTERNAL_STORAGE_MODEL_FILE_IDS as U, resolveUploadMimeType as V, getContentSha256 as W, buildArweaveContentTags as X, getItemStorageSubdir as Y, ZERO_BYTES32 as Z, getItemStorageLocalFileName as _, TProperty as a, registerClientActor as a$, INTERNAL_PROPERTY_NAMES as a0, defaultAttestationData as a1, definitionOnlyFields as a2, persistDefinitionFields as a3, getPropertyDefinition as a4, propertyDefinitionFromRow as a5, addCachedPropertyRefresher as a6, convertTxIdToImage as a7, ADDRESSES_PERSISTED_EVENT as a8, CONTENT_SHA256_TAG as a9, sniffMimeType as aA, notifyFilesWrittenOutsideCache as aB, createSessionObjectUrl as aC, withSeedDbLock as aD, countDbStatements as aE, startChangeFeed as aF, countLiveQueryRuns as aG, stopChangeFeed as aH, writeModelToDb as aI, writePropertyToDb as aJ, currentNameMatchLevels as aK, renameModelInDb as aL, postTabMessage as aM, onTabMessage as aN, listenForOtherTabsEvents as aO, startTabCoordination as aP, isTestOrSeedDev as aQ, insertModelStubs as aR, warnOnInvalidNameMatch as aS, refreshCachedProperties as aT, tryWithTabLock as aU, notifyFileSaved as aV, ClientManagerEvents as aW, getWatchedAddressesFromDb as aX, getOwnedAddressesFromDb as aY, CLIENT_NOT_INITIALIZED as aZ, stopTabCoordination as a_, CONTENT_TYPE_TAG as aa, ClientManagerState as ab, FILES_CHANGED_EVENT as ac, ImageSize as ad, LOCAL_COPIES_REMOVED_EVENT as ae, MachineIds as af, SeedModels as ag, TabLockTimeoutError as ah, VERSION_SCHEMA_UID_OPTIMISM_SEPOLIA as ai, closestImageSize as aj, contentSha256TagMatches as ak, ensureImageLocal as al, getAddressesForItemsFilter as am, isChangeFeedRunning as an, isClientReadySnapshot as ao, isLeaderTab as ap, normalizeAddressConfig as aq, normalizeAddressList as ar, normalizeArweaveTxIdForEnsure as as, parseAddressesPersistedPayload as at, watchEntityChanges as au, watchEntityRows as av, whenDbReady as aw, whenLeaderTab as ax, withDbTabLock as ay, MIME_TO_EXTENSION as az, TPropertyConstructor as b, PROPERTY_NAMES_EXEMPT_FROM_ID_SUFFIX_STRIP as b0, TAB_ID as b1, getModelSchemaName as b2, normalizeMimeType as b3, findFileInDir as b4, getStorageSubdirForDataType as b5, timingMarks as b6, withTabLock as b7, revokeSessionObjectUrl as b8, getPropertyDefinitionsByFileId as b9, timed as ba, isSessionObjectUrl as bb, timingNow as bc, timingSince as bd, timingLogger as be, timedDb as bf, findSeedRow$1 as bg, resolveModelRecord$1 as bh, propertyDefinition as bi, definitions as bj, db as bk, generateId$1 as bl, download as bm, findFile as bn, ensureImageLocal$1 as bo, tabEvents as bp, TPropertyDataType as c, TPropertyDefs as d, TStorageType as e, TValidationRules as f, normalizePropertyRecordSchema as g, eventEmitter as h, isDataType as i, findSeedRow as j, getPropertyDefinitions as k, resolveModelRecordByNameOrType as l, generateId as m, normalizeDataType as n, addSchemaToDb as o, isInternalSchema as p, inDefinitionsTransaction as q, resolveModelRecord as r, isClientReady as s, broadcastSchemasInvalidate as t, upsertSchemaRow as u, INTERNAL_DATA_TYPES as v, writeInBatches as w, normalizePublisher as x, chunkValues as y, rowsPerInsert as z };
3830
- //# sourceMappingURL=tabEvents-DT5ez6io.js.map