@seedprotocol/sdk 0.7.3-next.654.g52217f6 → 0.7.3

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 (187) hide show
  1. package/dist/{db-D9K_f5cF.js → BasePathResolver-Cm4apKRb.js} +2041 -127
  2. package/dist/BasePathResolver-Cm4apKRb.js.map +1 -0
  3. package/dist/{ClientManager-C-QXqUK9.js → ClientManager-i3nJASkg.js} +249 -147
  4. package/dist/ClientManager-i3nJASkg.js.map +1 -0
  5. package/dist/{Item-C7sUUu2B.js → Item-DBFchXpC.js} +259 -169
  6. package/dist/Item-DBFchXpC.js.map +1 -0
  7. package/dist/{ModelProperty-DJxeOOrc.js → ModelProperty-BS49g01Y.js} +166 -47
  8. package/dist/ModelProperty-BS49g01Y.js.map +1 -0
  9. package/dist/{Schema-fXtvd-yL.js → Schema-CpMNEnUO.js} +53 -42
  10. package/dist/Schema-CpMNEnUO.js.map +1 -0
  11. package/dist/{SchemaValidationService-CvRS2HSO.js → SchemaValidationService-DieV4KL-.js} +15 -17
  12. package/dist/{SchemaValidationService-CvRS2HSO.js.map → SchemaValidationService-DieV4KL-.js.map} +1 -1
  13. package/dist/{backfillMetadataPropertyIds-SXW-8C64.js → backfillMetadataPropertyIds-DSoOoTnX.js} +14 -7
  14. package/dist/{backfillMetadataPropertyIds-SXW-8C64.js.map → backfillMetadataPropertyIds-DSoOoTnX.js.map} +1 -1
  15. package/dist/{configurePlatform-BRw2j0ZN.js → configurePlatform-D0lQGAMQ.js} +5 -7
  16. package/dist/{configurePlatform-BRw2j0ZN.js.map → configurePlatform-D0lQGAMQ.js.map} +1 -1
  17. package/dist/detachRowsBeforeDelete-S0q-DJPT.js +50 -0
  18. package/dist/detachRowsBeforeDelete-S0q-DJPT.js.map +1 -0
  19. package/dist/{eas-DHxfswCt.js → eas-6pjU-OuX.js} +6 -8
  20. package/dist/eas-6pjU-OuX.js.map +1 -0
  21. package/dist/{getItem-d6N_aM6S.js → getItem-CxBNZ-WL.js} +10 -11
  22. package/dist/{getItem-d6N_aM6S.js.map → getItem-CxBNZ-WL.js.map} +1 -1
  23. package/dist/{getPublishPayload-BHrxVtzY.js → getPublishPayload-CTCvRUoU.js} +50 -18
  24. package/dist/getPublishPayload-CTCvRUoU.js.map +1 -0
  25. package/dist/{getPublishUploads-BEqRiKIS.js → getPublishUploads-SFHCbf_y.js} +13 -22
  26. package/dist/{getPublishUploads-BEqRiKIS.js.map → getPublishUploads-SFHCbf_y.js.map} +1 -1
  27. package/dist/{index-s15q14rm.js → index-8O3Uow-Y.js} +125 -57
  28. package/dist/index-8O3Uow-Y.js.map +1 -0
  29. package/dist/{index-DglvthOk.js → index-AVFTVp1m.js} +2 -2
  30. package/dist/index-AVFTVp1m.js.map +1 -0
  31. package/dist/index-Cvt5UBz4.js +30 -0
  32. package/dist/index-Cvt5UBz4.js.map +1 -0
  33. package/dist/{index.browser-Cm06tP4X.js → index.browser-DBhxRoe8.js} +11 -10
  34. package/dist/{index.browser-Cm06tP4X.js.map → index.browser-DBhxRoe8.js.map} +1 -1
  35. package/dist/{index.node-C30dmFgn.js → index.node-CJhlcNHM.js} +25 -13
  36. package/dist/index.node-CJhlcNHM.js.map +1 -0
  37. package/dist/main.js +27 -32
  38. package/dist/main.js.map +1 -1
  39. package/dist/node.js +19 -22
  40. package/dist/node.js.map +1 -1
  41. package/dist/platform.browser.js +9 -9
  42. package/dist/platform.node.js +18 -5
  43. package/dist/platform.node.js.map +1 -1
  44. package/dist/{property-DhmmYAvQ.js → property-DwPD283A.js} +11 -12
  45. package/dist/property-DwPD283A.js.map +1 -0
  46. package/dist/{publishDraftGraph-yYP74WRt.js → publishDraftGraph-BOn3_Occ.js} +6 -9
  47. package/dist/{publishDraftGraph-yYP74WRt.js.map → publishDraftGraph-BOn3_Occ.js.map} +1 -1
  48. package/dist/{resetAmbiguousLegacyItemData-Bk0QyDW8.js → resetAmbiguousLegacyItemData-BkYmBeJc.js} +14 -3
  49. package/dist/{resetAmbiguousLegacyItemData-Bk0QyDW8.js.map → resetAmbiguousLegacyItemData-BkYmBeJc.js.map} +1 -1
  50. package/dist/{resolveModelForSyncedSeed-8cAzvUFd.js → resolveModelForSyncedSeed-BL24nLa7.js} +2 -2
  51. package/dist/{resolveModelForSyncedSeed-8cAzvUFd.js.map → resolveModelForSyncedSeed-BL24nLa7.js.map} +1 -1
  52. package/dist/src/Item/Item.d.ts +20 -2
  53. package/dist/src/Item/Item.d.ts.map +1 -1
  54. package/dist/src/Item/service/actors/runPublish.d.ts +11 -2
  55. package/dist/src/Item/service/actors/runPublish.d.ts.map +1 -1
  56. package/dist/src/Item/service/itemMachineSingle.d.ts +12 -2
  57. package/dist/src/Item/service/itemMachineSingle.d.ts.map +1 -1
  58. package/dist/src/ItemProperty/ItemProperty.d.ts +5 -3
  59. package/dist/src/ItemProperty/ItemProperty.d.ts.map +1 -1
  60. package/dist/src/ItemProperty/service/actors/saveValueToDb/analyzeInput.d.ts.map +1 -1
  61. package/dist/src/ItemProperty/service/propertyMachine.d.ts.map +1 -1
  62. package/dist/src/Model/Model.d.ts +10 -4
  63. package/dist/src/Model/Model.d.ts.map +1 -1
  64. package/dist/src/Model/index.d.ts +2 -2
  65. package/dist/src/ModelProperty/ModelProperty.d.ts +10 -2
  66. package/dist/src/ModelProperty/ModelProperty.d.ts.map +1 -1
  67. package/dist/src/ModelProperty/service/modelPropertyMachine.d.ts +3 -0
  68. package/dist/src/ModelProperty/service/modelPropertyMachine.d.ts.map +1 -1
  69. package/dist/src/Schema/Schema.d.ts +2 -2
  70. package/dist/src/Schema/Schema.d.ts.map +1 -1
  71. package/dist/src/Schema/index.d.ts +0 -1
  72. package/dist/src/Schema/index.d.ts.map +1 -1
  73. package/dist/src/client/ClientManager.d.ts +30 -11
  74. package/dist/src/client/ClientManager.d.ts.map +1 -1
  75. package/dist/src/client/actors/platformClassesInit.d.ts +6 -0
  76. package/dist/src/client/actors/platformClassesInit.d.ts.map +1 -1
  77. package/dist/src/client/actors/processSchemaFiles.d.ts.map +1 -1
  78. package/dist/src/client/actors/saveConfig.d.ts +13 -6
  79. package/dist/src/client/actors/saveConfig.d.ts.map +1 -1
  80. package/dist/src/db/read/getPublishPayload.d.ts +1 -1
  81. package/dist/src/db/read/getPublishPayload.d.ts.map +1 -1
  82. package/dist/src/db/write/claimUnpublishedDrafts.d.ts +2 -0
  83. package/dist/src/db/write/claimUnpublishedDrafts.d.ts.map +1 -1
  84. package/dist/src/eventBus.d.ts +5 -0
  85. package/dist/src/eventBus.d.ts.map +1 -1
  86. package/dist/src/helpers/cachedPropertyRefresh.d.ts +12 -0
  87. package/dist/src/helpers/cachedPropertyRefresh.d.ts.map +1 -0
  88. package/dist/src/helpers/db.d.ts +3 -0
  89. package/dist/src/helpers/db.d.ts.map +1 -1
  90. package/dist/src/helpers/easSdk.d.ts +16 -0
  91. package/dist/src/helpers/easSdk.d.ts.map +1 -0
  92. package/dist/src/helpers/entity/detachRowsBeforeDelete.d.ts +16 -0
  93. package/dist/src/helpers/entity/detachRowsBeforeDelete.d.ts.map +1 -0
  94. package/dist/src/helpers/entity/entityDestroy.d.ts +2 -0
  95. package/dist/src/helpers/entity/entityDestroy.d.ts.map +1 -1
  96. package/dist/src/helpers/ownership.d.ts +9 -1
  97. package/dist/src/helpers/ownership.d.ts.map +1 -1
  98. package/dist/src/helpers/property/index.d.ts +37 -51
  99. package/dist/src/helpers/property/index.d.ts.map +1 -1
  100. package/dist/src/helpers/publishConfig.d.ts +27 -8
  101. package/dist/src/helpers/publishConfig.d.ts.map +1 -1
  102. package/dist/src/helpers/updateSchema.d.ts +5 -0
  103. package/dist/src/helpers/updateSchema.d.ts.map +1 -1
  104. package/dist/src/helpers/waitForEntityIdle.d.ts +5 -0
  105. package/dist/src/helpers/waitForEntityIdle.d.ts.map +1 -1
  106. package/dist/src/imports/json.d.ts +9 -0
  107. package/dist/src/imports/json.d.ts.map +1 -1
  108. package/dist/src/index.d.ts +6 -4
  109. package/dist/src/index.d.ts.map +1 -1
  110. package/dist/src/interfaces/IItem.d.ts +7 -1
  111. package/dist/src/interfaces/IItem.d.ts.map +1 -1
  112. package/dist/src/node/db/Db.d.ts.map +1 -1
  113. package/dist/src/node/helpers/PathResolver.d.ts.map +1 -1
  114. package/dist/src/node/index.d.ts +8 -1
  115. package/dist/src/node/index.d.ts.map +1 -1
  116. package/dist/src/platform/index.browser.d.ts +2 -0
  117. package/dist/src/platform/index.browser.d.ts.map +1 -1
  118. package/dist/src/platform/index.node.d.ts +2 -0
  119. package/dist/src/platform/index.node.d.ts.map +1 -1
  120. package/dist/src/types/index.d.ts +12 -1
  121. package/dist/src/types/index.d.ts.map +1 -1
  122. package/dist/src/types/machines.d.ts +2 -0
  123. package/dist/src/types/machines.d.ts.map +1 -1
  124. package/dist/src/types/property.d.ts +8 -0
  125. package/dist/src/types/property.d.ts.map +1 -1
  126. package/dist/{syncDbWithEas--HUA9WNb.js → syncDbWithEas-Bv2Fe38J.js} +15 -16
  127. package/dist/syncDbWithEas-Bv2Fe38J.js.map +1 -0
  128. package/dist/tabEvents-9s2WCYw-.js +335 -0
  129. package/dist/tabEvents-9s2WCYw-.js.map +1 -0
  130. package/dist/{tolerantWebAccess-BZYWt09Y.js → tolerantWebAccess-iUu80pAP.js} +9 -9
  131. package/dist/{tolerantWebAccess-BZYWt09Y.js.map → tolerantWebAccess-iUu80pAP.js.map} +1 -1
  132. package/dist/{updateSeedRevokedAt-D_EG-q3b.js → updateSeedRevokedAt-zBfi931j.js} +3 -3
  133. package/dist/{updateSeedRevokedAt-D_EG-q3b.js.map → updateSeedRevokedAt-zBfi931j.js.map} +1 -1
  134. package/package.json +11 -12
  135. package/src/node/easLodashRegister.js +4 -0
  136. package/dist/BasePathResolver-CfSfMW5_.js +0 -676
  137. package/dist/BasePathResolver-CfSfMW5_.js.map +0 -1
  138. package/dist/ClientManager-C-QXqUK9.js.map +0 -1
  139. package/dist/Item-C7sUUu2B.js.map +0 -1
  140. package/dist/ModelProperty-DJxeOOrc.js.map +0 -1
  141. package/dist/Schema-fXtvd-yL.js.map +0 -1
  142. package/dist/cjs/ModelProperty-DXZ39Wyv.js +0 -1978
  143. package/dist/cjs/ModelProperty-DXZ39Wyv.js.map +0 -1
  144. package/dist/cjs/Schema-DdP109QD.js +0 -5257
  145. package/dist/cjs/Schema-DdP109QD.js.map +0 -1
  146. package/dist/cjs/SchemaValidationService-CSXxx7sQ.js +0 -779
  147. package/dist/cjs/SchemaValidationService-CSXxx7sQ.js.map +0 -1
  148. package/dist/cjs/backfillMetadataPropertyIds-CPYoaMl4.js +0 -90
  149. package/dist/cjs/backfillMetadataPropertyIds-CPYoaMl4.js.map +0 -1
  150. package/dist/cjs/eas-B2YDQssV.js +0 -139
  151. package/dist/cjs/eas-B2YDQssV.js.map +0 -1
  152. package/dist/cjs/getItem-DgnaQzbE.js +0 -59
  153. package/dist/cjs/getItem-DgnaQzbE.js.map +0 -1
  154. package/dist/cjs/getPublishPayload-CqlqRdot.js +0 -1604
  155. package/dist/cjs/getPublishPayload-CqlqRdot.js.map +0 -1
  156. package/dist/cjs/getPublishUploads-DsXY9S4U.js +0 -872
  157. package/dist/cjs/getPublishUploads-DsXY9S4U.js.map +0 -1
  158. package/dist/cjs/index-BHaWOwNF.js +0 -71
  159. package/dist/cjs/index-BHaWOwNF.js.map +0 -1
  160. package/dist/cjs/index-C4XlgCJ4.js +0 -22801
  161. package/dist/cjs/index-C4XlgCJ4.js.map +0 -1
  162. package/dist/cjs/index.browser-BSf8HpnU.js +0 -22
  163. package/dist/cjs/index.browser-BSf8HpnU.js.map +0 -1
  164. package/dist/cjs/property-fa0iju0J.js +0 -328
  165. package/dist/cjs/property-fa0iju0J.js.map +0 -1
  166. package/dist/cjs/publishDraftGraph-3ASwAlD7.js +0 -403
  167. package/dist/cjs/publishDraftGraph-3ASwAlD7.js.map +0 -1
  168. package/dist/cjs/resetAmbiguousLegacyItemData-Bu_MBBYt.js +0 -117
  169. package/dist/cjs/resetAmbiguousLegacyItemData-Bu_MBBYt.js.map +0 -1
  170. package/dist/cjs/resolveModelForSyncedSeed-BANIiuFU.js +0 -49
  171. package/dist/cjs/resolveModelForSyncedSeed-BANIiuFU.js.map +0 -1
  172. package/dist/cjs/syncDbWithEas-CrmxxmUW.js +0 -887
  173. package/dist/cjs/syncDbWithEas-CrmxxmUW.js.map +0 -1
  174. package/dist/db-D9K_f5cF.js.map +0 -1
  175. package/dist/eas-DHxfswCt.js.map +0 -1
  176. package/dist/getPublishPayload-BHrxVtzY.js.map +0 -1
  177. package/dist/index-CiMEoGu3.js +0 -32
  178. package/dist/index-CiMEoGu3.js.map +0 -1
  179. package/dist/index-DglvthOk.js.map +0 -1
  180. package/dist/index-s15q14rm.js.map +0 -1
  181. package/dist/index.node-C30dmFgn.js.map +0 -1
  182. package/dist/main.cjs +0 -85
  183. package/dist/main.cjs.map +0 -1
  184. package/dist/property-DhmmYAvQ.js.map +0 -1
  185. package/dist/syncDbWithEas--HUA9WNb.js.map +0 -1
  186. package/dist/tabEvents-DguZpCCv.js +0 -1580
  187. package/dist/tabEvents-DguZpCCv.js.map +0 -1
@@ -1,1580 +0,0 @@
1
- import debug from 'debug';
2
- import { B as BaseDb, m as metadata, t as BaseFileManager, a as appState, D as ImageSize, E as EAS_SEED_DATA_SYNCED_TO_DB_EVENT } from './BasePathResolver-CfSfMW5_.js';
3
- import { isEasAttestationExplorerUrl, BaseArweaveClient, ensureReadGatewaySelected, getArweaveReadBaseUrls, getTransactionTagsByIds } from '@seedprotocol/arweave';
4
- import { BaseQueryClient, BaseEasClient, GET_FILES_METADATA } from '@seedprotocol/eas';
5
- import { waitFor } from 'xstate';
6
- import 'pluralize';
7
- import { n as normalizeDataType, t as getAllAddressesFromDb, m as eventEmitter } from './db-D9K_f5cF.js';
8
- import { and, eq, isNotNull, inArray, or } from 'drizzle-orm';
9
- import { customAlphabet } from 'nanoid';
10
- import { alphanumeric } from 'nanoid-dictionary';
11
- import '@seedprotocol/query';
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
- /** Same rules as getCorrectId in helpers/index (avoid barrel circular import). */
18
- function parseLocalIdOrUid(localIdOrUid) {
19
- const id = { localId: undefined, uid: undefined };
20
- if (!localIdOrUid)
21
- return id;
22
- if (localIdOrUid.length === 10) {
23
- id.localId = localIdOrUid;
24
- }
25
- if (localIdOrUid.startsWith('0x') && localIdOrUid.length === 66) {
26
- id.uid = localIdOrUid;
27
- }
28
- return id;
29
- }
30
- /** Relation/image refs may be string ids or `{ seedLocalId?, seedUid? }`. */
31
- function normalizeRelationPropertyValue(value) {
32
- if (value == null)
33
- return undefined;
34
- if (typeof value === 'string') {
35
- const t = value.trim();
36
- return t !== '' ? t : undefined;
37
- }
38
- if (typeof value === 'object' && value !== null) {
39
- const o = value;
40
- if (typeof o.seedLocalId === 'string' && o.seedLocalId.trim() !== '') {
41
- return o.seedLocalId.trim();
42
- }
43
- if (typeof o.seedUid === 'string' && o.seedUid.trim() !== '') {
44
- return o.seedUid.trim();
45
- }
46
- }
47
- return undefined;
48
- }
49
- /** getCorrectId only accepts 10-char local ids and 0x66 uids; local seed refs can be up to 21 chars. */
50
- const LOCAL_SEED_REF = /^[a-zA-Z0-9_-]{10,21}$/;
51
- /** Resolve seed ids from a single string (context propertyValue or DB row). */
52
- function resolveSeedIdsFromRefString(s) {
53
- const fromCorrect = parseLocalIdOrUid(s);
54
- if (fromCorrect.localId || fromCorrect.uid) {
55
- return { seedLocalId: fromCorrect.localId, seedUid: fromCorrect.uid };
56
- }
57
- if (LOCAL_SEED_REF.test(s) && !s.startsWith('0x')) {
58
- return { seedLocalId: s };
59
- }
60
- return {};
61
- }
62
- /**
63
- * True when a relation ref is an attested seed uid (0x…) rather than a local id. A uid only exists once
64
- * the seed is on EAS, so publish can attest the uid as-is when no local copy exists (e.g. a server
65
- * publishing on someone's behalf that only holds the uids it was given). Callers must not walk into it.
66
- */
67
- function isPublishedSeedRef(value) {
68
- const ref = normalizeRelationPropertyValue(value);
69
- if (!ref)
70
- return false;
71
- const { seedLocalId, seedUid } = resolveSeedIdsFromRefString(ref);
72
- return !!seedUid && !seedLocalId;
73
- }
74
- /**
75
- * Storage seed local id (nanoid) or 0x uid. Stricter than resolveSeedIdsFromRefString, which accepts
76
- * any 10-char string (so a 10-char html snippet like "<p>raw</p>" would pass as a local id).
77
- */
78
- const STORAGE_SEED_REF = /^(?:[A-Za-z0-9_-]{10,21}|0x[0-9a-fA-F]{64})$/;
79
- /** True when a File/Image/Html value is already a storage seed reference rather than raw content. */
80
- function isStorageSeedRef(value) {
81
- return typeof value === 'string' && STORAGE_SEED_REF.test(value.trim());
82
- }
83
-
84
- const seedUidToStorageTransactionId = new Map();
85
- const getStorageTransactionIdForSeedUid = async (seedUid) => {
86
- if (seedUidToStorageTransactionId.has(seedUid)) {
87
- return seedUidToStorageTransactionId.get(seedUid);
88
- }
89
- const appDb = BaseDb.getAppDb();
90
- const results = (await appDb
91
- .select({
92
- storageTransactionId: metadata.propertyValue,
93
- })
94
- .from(metadata)
95
- .where(and(eq(metadata.seedUid, seedUid), eq(metadata.propertyName, 'storageTransactionId'))));
96
- if (!results || results.length === 0) {
97
- return;
98
- }
99
- seedUidToStorageTransactionId.set(seedUid, results[0].storageTransactionId);
100
- return results[0].storageTransactionId;
101
- };
102
-
103
- const logger$5 = debug('seedSdk:helpers:mediaRef');
104
- /** Typical Arweave transaction id: 43 URL-safe base64 characters. */
105
- const ARWEAVE_TX_ID = /^[a-zA-Z0-9_-]{43}$/;
106
- function trimOrEmpty(raw) {
107
- if (raw == null)
108
- return '';
109
- return typeof raw === 'string' ? raw.trim() : String(raw).trim();
110
- }
111
- function coalesceStringField(item, key) {
112
- const v = item[key];
113
- if (v == null)
114
- return '';
115
- if (typeof v === 'string')
116
- return v.trim();
117
- if (typeof v === 'number' || typeof v === 'boolean')
118
- return String(v);
119
- return '';
120
- }
121
- /**
122
- * Try JSON relation payloads and plain relation id strings.
123
- */
124
- function unwrapRelationString(raw) {
125
- const t = raw.trim();
126
- if (!t)
127
- return t;
128
- try {
129
- const parsed = JSON.parse(t);
130
- const fromObj = normalizeRelationPropertyValue(parsed);
131
- if (fromObj)
132
- return fromObj;
133
- }
134
- catch {
135
- // not JSON
136
- }
137
- return t;
138
- }
139
- /**
140
- * Pure, sync classification of a media-related string from feeds or XML.
141
- */
142
- function classifyMediaRef(raw, options) {
143
- const s = trimOrEmpty(raw);
144
- if (!s) {
145
- return { kind: 'empty' };
146
- }
147
- const treatAs = options?.treatAs;
148
- if (treatAs === 'url') {
149
- return { kind: 'url', href: s };
150
- }
151
- if (treatAs === 'arweaveTx') {
152
- return { kind: 'arweaveTxId', txId: s };
153
- }
154
- if (treatAs === 'seedUid') {
155
- if (s.startsWith('0x') && s.length === 66) {
156
- return { kind: 'seedUid', uid: s };
157
- }
158
- return { kind: 'unknown', raw: s };
159
- }
160
- if (s.startsWith('http://') || s.startsWith('https://') || s.startsWith('blob:') || s.startsWith('data:')) {
161
- if (isEasAttestationExplorerUrl(s)) {
162
- return { kind: 'nonMediaUrl', href: s };
163
- }
164
- return { kind: 'url', href: s };
165
- }
166
- const unwrapped = unwrapRelationString(s);
167
- const ids = resolveSeedIdsFromRefString(unwrapped);
168
- if (ids.seedUid) {
169
- return { kind: 'seedUid', uid: ids.seedUid };
170
- }
171
- if (ids.seedLocalId) {
172
- return { kind: 'seedLocalId', localId: ids.seedLocalId };
173
- }
174
- if (unwrapped.startsWith('0x') && unwrapped.length === 66) {
175
- return { kind: 'seedUid', uid: unwrapped };
176
- }
177
- if (ARWEAVE_TX_ID.test(unwrapped) && !unwrapped.startsWith('0x')) {
178
- return { kind: 'arweaveTxId', txId: unwrapped };
179
- }
180
- return { kind: 'unknown', raw: s };
181
- }
182
- /**
183
- * Same as convertTxIdToImage but local to this module to avoid circular imports with helpers/index.
184
- */
185
- async function tryLocalBlobUrlFromTxId(txId) {
186
- const imageFilePath = BaseFileManager.getFilesPath('images', txId);
187
- const fileExists = await BaseFileManager.pathExists(imageFilePath);
188
- if (!fileExists) {
189
- logger$5(`[tryLocalBlobUrlFromTxId] ${imageFilePath} does not exist`);
190
- return undefined;
191
- }
192
- const buffer = await BaseFileManager.readFileAsBuffer(imageFilePath);
193
- let arrayBuffer;
194
- if (buffer instanceof Blob) {
195
- arrayBuffer = await buffer.arrayBuffer();
196
- }
197
- else if (buffer instanceof ArrayBuffer) {
198
- arrayBuffer = buffer;
199
- }
200
- else {
201
- const sliced = buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
202
- if (sliced instanceof SharedArrayBuffer) {
203
- const uint8Array = new Uint8Array(sliced);
204
- arrayBuffer = uint8Array.buffer.slice(0);
205
- }
206
- else {
207
- arrayBuffer = sliced;
208
- }
209
- }
210
- const uint = new Uint8Array(arrayBuffer);
211
- const imageBlob = new Blob([uint]);
212
- return URL.createObjectURL(imageBlob);
213
- }
214
- async function resolveTxId(txId) {
215
- const local = await tryLocalBlobUrlFromTxId(txId);
216
- if (local) {
217
- return { status: 'ready', href: local, source: 'localBlob' };
218
- }
219
- return {
220
- status: 'ready',
221
- href: BaseArweaveClient.getRawUrl(txId),
222
- source: 'gateway',
223
- };
224
- }
225
- /**
226
- * Resolve a classified media reference to a display URL (https/blob/data), using local files when present.
227
- */
228
- async function resolveMediaRef(raw, options) {
229
- const classification = classifyMediaRef(raw, options);
230
- switch (classification.kind) {
231
- case 'empty':
232
- return { status: 'empty' };
233
- case 'url':
234
- return {
235
- status: 'ready',
236
- href: classification.href,
237
- source: 'direct',
238
- };
239
- case 'nonMediaUrl':
240
- return {
241
- status: 'unresolved',
242
- reason: 'non_media_url',
243
- classification,
244
- };
245
- case 'seedLocalId':
246
- return {
247
- status: 'unresolved',
248
- reason: 'seed_local_id_not_portable',
249
- classification,
250
- };
251
- case 'unknown':
252
- return {
253
- status: 'unresolved',
254
- reason: 'unknown_ref',
255
- classification,
256
- };
257
- case 'arweaveTxId':
258
- return resolveTxId(classification.txId);
259
- case 'seedUid': {
260
- const appDb = BaseDb.getAppDb();
261
- if (!appDb) {
262
- return {
263
- status: 'unresolved',
264
- reason: 'seed_uid_requires_app_db',
265
- classification,
266
- };
267
- }
268
- const storageTransactionId = await getStorageTransactionIdForSeedUid(classification.uid);
269
- if (!storageTransactionId) {
270
- return {
271
- status: 'unresolved',
272
- reason: 'seed_uid_no_storage_transaction',
273
- classification,
274
- };
275
- }
276
- return resolveTxId(storageTransactionId);
277
- }
278
- default:
279
- return {
280
- status: 'unresolved',
281
- reason: 'unknown_ref',
282
- classification,
283
- };
284
- }
285
- }
286
- /**
287
- * Read a string field from a parsed item, trying camelCase and snake_case keys.
288
- */
289
- function getFeedItemStringField(item, key) {
290
- const direct = coalesceStringField(item, key);
291
- if (direct)
292
- return direct;
293
- const snake = key.replace(/[A-Z]/g, (l) => `_${l.toLowerCase()}`);
294
- if (snake !== key) {
295
- const alt = coalesceStringField(item, snake);
296
- if (alt)
297
- return alt;
298
- }
299
- const parts = key.split('_');
300
- if (parts.length > 1) {
301
- const camel = parts[0] + parts.slice(1).map((p) => p.charAt(0).toUpperCase() + p.slice(1)).join('');
302
- const alt2 = coalesceStringField(item, camel);
303
- if (alt2)
304
- return alt2;
305
- }
306
- return '';
307
- }
308
- /**
309
- * Apply a field manifest to a plain feed item: classify media/file fields; pass through html/text.
310
- */
311
- function normalizeFeedItemFields(item, manifest) {
312
- const out = {};
313
- for (const [fieldKey, descriptor] of Object.entries(manifest)) {
314
- const raw = getFeedItemStringField(item, fieldKey);
315
- if (!raw) {
316
- out[fieldKey] = undefined;
317
- continue;
318
- }
319
- if (descriptor.role === 'html') {
320
- out[fieldKey] = { role: 'html', raw };
321
- continue;
322
- }
323
- if (descriptor.role === 'text') {
324
- out[fieldKey] = { role: 'text', raw };
325
- continue;
326
- }
327
- const classification = classifyMediaRef(raw, { treatAs: descriptor.treatAs });
328
- out[fieldKey] = {
329
- role: descriptor.role,
330
- raw,
331
- classification,
332
- };
333
- }
334
- return out;
335
- }
336
-
337
- const getMetadata = async (props) => {
338
- const appDb = BaseDb.getAppDb();
339
- let storageTransactionId;
340
- if (props) {
341
- storageTransactionId = props.storageTransactionId;
342
- }
343
- const whereClauses = [];
344
- if (storageTransactionId) {
345
- whereClauses.push(eq(metadata.propertyName, 'storageTransactionId'));
346
- whereClauses.push(eq(metadata.propertyValue, storageTransactionId));
347
- }
348
- const metadataRows = await appDb
349
- .select()
350
- .from(metadata)
351
- .where(and(...whereClauses));
352
- if (!metadataRows || metadataRows.length === 0) {
353
- return;
354
- }
355
- return metadataRows[0];
356
- };
357
-
358
- const saveMetadata = async (metadataRecord, metadataValues) => {
359
- if (!metadataRecord.localId) {
360
- return;
361
- }
362
- const appDb = BaseDb.getAppDb();
363
- await appDb
364
- .update(metadata)
365
- .set({
366
- ...metadataValues,
367
- updatedAt: Date.now(),
368
- })
369
- .where(eq(metadata.localId, metadataRecord.localId));
370
- };
371
-
372
- const isNode = () => {
373
- return (typeof process !== 'undefined' &&
374
- process.versions != null &&
375
- process.versions.node != null);
376
- };
377
- const isBrowser = () => {
378
- return !isElectron() && typeof document !== 'undefined' && typeof window !== 'undefined';
379
- };
380
- const isElectron = () => {
381
- return typeof process !== 'undefined' && process.versions != null && process.versions.electron != null;
382
- };
383
- /**
384
- * Detects if code is running in an Electron renderer process.
385
- *
386
- * Uses multiple detection methods for robustness:
387
- * 1. Checks process.type === 'renderer' (most reliable)
388
- * 2. Falls back to checking if Electron is present and window/document exist
389
- *
390
- * @returns true if running in Electron renderer process, false otherwise
391
- */
392
- const isElectronRenderer = () => {
393
- // Method 1: Check process.type (most reliable - official Electron API)
394
- if (typeof process !== 'undefined' &&
395
- process.type === 'renderer') {
396
- return true;
397
- }
398
- // Method 2: Fallback - Electron + browser-like environment
399
- // Renderer processes have window/document, main process doesn't (in modern Electron)
400
- if (isElectron() &&
401
- typeof window !== 'undefined' &&
402
- typeof document !== 'undefined') {
403
- return true;
404
- }
405
- return false;
406
- };
407
- /**
408
- * True where OPFS-backed Arweave file downloads should run: normal browsers and
409
- * Electron renderer. {@link isBrowser} is false for every Electron build (including renderer).
410
- */
411
- const supportsOpfsFileDownloads = () => {
412
- return isBrowser() || isElectronRenderer();
413
- };
414
-
415
- /**
416
- * Tag every Seed upload carries with the hex digest of its data. Producers: the SDK's
417
- * `getPublishUploadData` / `getPublishUploads` (and through them both the L1 transaction path and
418
- * the bundler / DataItem path in `@seedprotocol/publish`). Consumer: the files download
419
- * (`events/files/download.ts` and the download worker), which verifies downloaded bytes against it
420
- * and copies a verified value into `metadata.content_hash`.
421
- */
422
- const CONTENT_SHA256_TAG = 'Content-SHA-256';
423
- const CONTENT_TYPE_TAG = 'Content-Type';
424
- /**
425
- * Lowercase hex SHA-256 of `data`: the value of the {@link CONTENT_SHA256_TAG} tag.
426
- *
427
- * Uses @noble/hashes rather than WebCrypto so it also works outside secure contexts (WebCrypto's
428
- * `crypto.subtle` is missing on plain-http origins); the digest is the same.
429
- *
430
- * In SDK 0.7.2 and earlier the L1 transaction path (`getPublishUploads`) wrote SHA3-256 under this tag name
431
- * while the bundler path wrote SHA-256. Uploads from that path still carry SHA3-256 digests; use
432
- * {@link contentSha256TagMatches} to check a tag value against bytes.
433
- */
434
- const getContentSha256 = (data) => bytesToHex(sha256(data));
435
- /**
436
- * True when `tagValue` (a {@link CONTENT_SHA256_TAG} value read from Arweave) is the digest of
437
- * `data`. Accepts SHA-256 and, for uploads made by SDK 0.7.2 and earlier through the L1 transaction path,
438
- * SHA3-256.
439
- */
440
- const contentSha256TagMatches = (data, tagValue) => {
441
- const expected = tagValue.trim().toLowerCase();
442
- if (!expected)
443
- return false;
444
- return getContentSha256(data) === expected || bytesToHex(sha3_256(data)) === expected;
445
- };
446
- /**
447
- * Tags for one upload, in the order every upload uses: content hash, content type, then any
448
- * configured extra tags (e.g. App-Name from `arweaveUploadTags`).
449
- */
450
- const buildArweaveContentTags = (contentHash, contentType, extra) => {
451
- const tags = [];
452
- if (contentHash)
453
- tags.push({ name: CONTENT_SHA256_TAG, value: contentHash });
454
- if (contentType)
455
- tags.push({ name: CONTENT_TYPE_TAG, value: contentType });
456
- if (extra?.length)
457
- tags.push(...extra);
458
- return tags;
459
- };
460
-
461
- /** Subdirectory of the files root that holds a storage seed's file, by its data type. */
462
- const getStorageSubdirForDataType = (dataType) => {
463
- switch (normalizeDataType(dataType ?? undefined)) {
464
- case 'Image':
465
- return 'images';
466
- case 'Html':
467
- return 'html';
468
- case 'Json':
469
- return 'json';
470
- case 'File':
471
- default:
472
- // Unknown types are generic files; `images/` is only for what the SDK knows is an image.
473
- return 'files';
474
- }
475
- };
476
- /** Absolute directory for a storage seed's file (`images/`, `files/`, `html/` or `json/` under the files root). */
477
- const getStorageDirForDataType = (dataType) => BaseFileManager.getFilesPath(getStorageSubdirForDataType(dataType));
478
- /**
479
- * Subdirectory (relative to the files root, no leading slash) for an ItemStorage property's file:
480
- * its model's `storage.path` (`localStorageDir`), else `files`.
481
- */
482
- const getItemStorageSubdir = (localStorageDir) => (localStorageDir ?? '').replace(/^\/+/, '').replace(/\/+$/, '') || 'files';
483
- /**
484
- * File name a local save gives an ItemStorage property: `<seedUid or seedLocalId><extension>`
485
- * (see saveItemStorage). Downloaded content is named after its transaction instead
486
- * (`<txId><extension>`, see syncDbWithEas), which is what the metadata row's refResolvedValue says.
487
- */
488
- const getItemStorageLocalFileName = (seedId, filenameSuffix) => `${seedId}${filenameSuffix ?? ''}`;
489
-
490
- /** Rows per query: well under SQLite's bound-parameter limit. */
491
- const CHUNK_SIZE = 400;
492
- /**
493
- * Where the download worker saves each child of a composite upload, by transaction id then child
494
- * (property) name: a path relative to the files root.
495
- *
496
- * The paths come from the ItemStorage rows sync derives from a `storage_transaction_id`
497
- * attestation (see `createMetadataRecordsForStorageTransactionId`): property value = the
498
- * transaction id, `refResolvedValue` = `<txId><extension>`, `localStorageDir` = the model's storage
499
- * directory. Those rows are what hydration reads, so a child saved here is found as that property's
500
- * file. Transactions with no such rows are left out; the worker then falls back to the legacy
501
- * html / json layout.
502
- */
503
- const getCompositeChildPaths = async (transactionIds) => {
504
- const out = {};
505
- const appDb = BaseDb.getAppDb();
506
- if (!appDb || transactionIds.length === 0)
507
- return out;
508
- for (let i = 0; i < transactionIds.length; i += CHUNK_SIZE) {
509
- const chunk = transactionIds.slice(i, i + CHUNK_SIZE);
510
- const rows = await appDb
511
- .select({
512
- transactionId: metadata.propertyValue,
513
- propertyName: metadata.propertyName,
514
- refResolvedValue: metadata.refResolvedValue,
515
- localStorageDir: metadata.localStorageDir,
516
- })
517
- .from(metadata)
518
- .where(and(isNotNull(metadata.derivedFromUid), isNotNull(metadata.refResolvedValue), inArray(metadata.propertyValue, chunk)));
519
- for (const row of rows) {
520
- const { transactionId, propertyName, refResolvedValue } = row;
521
- if (!transactionId || !propertyName || !refResolvedValue)
522
- continue;
523
- // A file name, never a path: anything else is not where the SDK would look.
524
- if (refResolvedValue.includes('/') || refResolvedValue.includes('\\'))
525
- continue;
526
- const byName = (out[transactionId] ?? (out[transactionId] = {}));
527
- byName[propertyName] ?? (byName[propertyName] = `${getItemStorageSubdir(row.localStorageDir)}/${refResolvedValue}`);
528
- }
529
- }
530
- return out;
531
- };
532
-
533
- const logger$4 = debug('seedSdk:files:download');
534
- const AUTO_BULK_DOWNLOAD_THROTTLE_MS = 10000;
535
- /** Avoid refetch-on-every-call: transient empty indexer responses were overwriting good cache (see trailing throttle ~10s after success). */
536
- const GET_FILES_METADATA_STALE_MS = 120000;
537
- let bulkDownloadInFlight = null;
538
- const lazyDownloadInFlightByTransactionId = new Map();
539
- // syncDbFiles helper - internal service removed, functionality moved here
540
- const syncDbFiles = async (endpoints) => {
541
- // TODO: Implement syncDbFiles functionality if needed
542
- // This was previously in @/services/internal/helpers
543
- logger$4('[download] syncDbFiles called but not yet implemented');
544
- return Promise.resolve();
545
- };
546
- const getAppDbReady = async () => {
547
- if (!BaseDb.isAppDbReady()) {
548
- // Wait for ClientManager to be ready (DB_INIT state or later)
549
- const _mod_0 = await import('./ClientManager-C-QXqUK9.js');
550
- const _ns_0 = _mod_0.J;
551
- const clientManagerMod = _ns_0;
552
- const { getClient } = clientManagerMod;
553
- const clientManager = getClient();
554
- const clientService = clientManager.getService();
555
- await waitFor(clientService, (snapshot) => {
556
- const state = snapshot.value;
557
- return state === 'dbInit' ||
558
- state === 'saveConfig' ||
559
- state === 'processSchemaFiles' ||
560
- state === 'addModelsToStore' ||
561
- state === 'addModelsToDb' ||
562
- state === 'idle';
563
- }, { timeout: 30000 });
564
- }
565
- return BaseDb.getAppDb();
566
- };
567
- const getExcludedTransactions = async () => {
568
- const appDb = await getAppDbReady();
569
- if (!appDb) {
570
- return new Set();
571
- }
572
- const excludedTransactionsQuery = await appDb
573
- .select()
574
- .from(appState)
575
- .where(eq(appState.key, 'excludedTransactions'));
576
- const excludedTransactions = new Set();
577
- if (excludedTransactionsQuery && excludedTransactionsQuery.length === 1) {
578
- const valueString = excludedTransactionsQuery[0].value;
579
- if (valueString) {
580
- const excludedTransactionsArray = JSON.parse(valueString);
581
- for (const txId of excludedTransactionsArray) {
582
- excludedTransactions.add(txId);
583
- }
584
- }
585
- }
586
- return excludedTransactions;
587
- };
588
- /** Arweave transaction ids are 43 chars in the URL-safe base64 alphabet. */
589
- const ARWEAVE_TX_ID_RE$1 = /^[a-z0-9_-]{43}$/i;
590
- const normalizeArweaveTxId = (raw) => {
591
- if (raw == null || raw === '')
592
- return undefined;
593
- const t = String(raw).trim();
594
- if (ARWEAVE_TX_ID_RE$1.test(t))
595
- return t;
596
- const m = t.match(/^([a-z0-9_-]{43})(?:\.[^/]+)?$/i);
597
- if (m)
598
- return m[1];
599
- // Values may be full gateway URLs or paths; scan for the embedded tx id.
600
- const embedded = t.match(/[a-z0-9_-]{43}/gi);
601
- if (embedded) {
602
- for (const candidate of embedded) {
603
- if (ARWEAVE_TX_ID_RE$1.test(candidate))
604
- return candidate;
605
- }
606
- }
607
- return undefined;
608
- };
609
- const mergeAddressLists = (a, b) => {
610
- const seen = new Set();
611
- const out = [];
612
- for (const addr of [...(a ?? []), ...(b ?? [])]) {
613
- if (!addr || !addr.trim())
614
- continue;
615
- const k = addr.toLowerCase();
616
- if (seen.has(k))
617
- continue;
618
- seen.add(k);
619
- out.push(addr);
620
- }
621
- return out;
622
- };
623
- const collectTransactionIdsFromLocalMetadata = async () => {
624
- const appDb = await getAppDbReady();
625
- if (!appDb) {
626
- return [];
627
- }
628
- const nameOr = or(eq(metadata.propertyName, 'storageTransactionId'), eq(metadata.propertyName, 'storage_transaction_id'), eq(metadata.propertyName, 'transactionId'));
629
- const propRows = await appDb
630
- .select({ propertyValue: metadata.propertyValue })
631
- .from(metadata)
632
- .where(and(nameOr, isNotNull(metadata.propertyValue)));
633
- const refRows = await appDb
634
- .select({ refResolvedValue: metadata.refResolvedValue })
635
- .from(metadata)
636
- .where(isNotNull(metadata.refResolvedValue));
637
- const ids = new Set();
638
- for (const row of propRows) {
639
- const id = normalizeArweaveTxId(row.propertyValue ?? undefined);
640
- if (id)
641
- ids.add(id);
642
- }
643
- for (const row of refRows) {
644
- const id = normalizeArweaveTxId(row.refResolvedValue ?? undefined);
645
- if (id)
646
- ids.add(id);
647
- }
648
- return Array.from(ids);
649
- };
650
- const parseTransactionIdsFromMetadata = (filesMetadata, excludedTransactions) => {
651
- const transactionIds = [];
652
- for (const fileMetadata of filesMetadata) {
653
- // Validate and parse decodedDataJson
654
- if (!fileMetadata.decodedDataJson || fileMetadata.decodedDataJson.trim() === '') {
655
- console.warn('[events/files] [download] empty decodedDataJson for fileMetadata: ', fileMetadata.id);
656
- continue;
657
- }
658
- let json;
659
- try {
660
- json = JSON.parse(fileMetadata.decodedDataJson);
661
- }
662
- catch (error) {
663
- console.warn('[events/files] [download] failed to parse decodedDataJson for fileMetadata: ', fileMetadata.id, error);
664
- continue;
665
- }
666
- if (!Array.isArray(json) || json.length === 0 || !json[0]?.value?.value) {
667
- console.warn('[events/files] [download] invalid decodedDataJson structure for fileMetadata: ', fileMetadata.id);
668
- continue;
669
- }
670
- const rawTx = json[0].value.value;
671
- if (typeof rawTx !== 'string' || rawTx.trim().length === 0) {
672
- continue;
673
- }
674
- const transactionId = normalizeArweaveTxId(rawTx.trim());
675
- if (!transactionId) {
676
- continue;
677
- }
678
- if (excludedTransactions.has(transactionId) || excludedTransactions.has(rawTx.trim())) {
679
- continue;
680
- }
681
- transactionIds.push(transactionId);
682
- }
683
- return transactionIds;
684
- };
685
- /** Content-SHA-256 tag values for the transactions that have one, from one batched tag lookup (cached). */
686
- const getExpectedContentHashes = async (transactionIds) => {
687
- const queryClient = BaseQueryClient.getQueryClient();
688
- const tagsById = new Map();
689
- const toFetch = [];
690
- for (const transactionId of transactionIds) {
691
- // Tags never change once a transaction is indexed, so a cached lookup stays good.
692
- const cached = queryClient.getQueryData?.(['getTransactionTags', transactionId]);
693
- if (cached)
694
- tagsById.set(transactionId, cached);
695
- else
696
- toFetch.push(transactionId);
697
- }
698
- if (toFetch.length > 0) {
699
- try {
700
- const fetched = await getTransactionTagsByIds(toFetch, {
701
- client: BaseArweaveClient.getArweaveClient(),
702
- onError: (error) => logger$4('[download] transaction tags lookup failed', error),
703
- });
704
- for (const [transactionId, tags] of fetched) {
705
- tagsById.set(transactionId, tags);
706
- // Seed the per-transaction cache entry (the client interface has no setQueryData).
707
- void queryClient
708
- .fetchQuery({ queryKey: ['getTransactionTags', transactionId], queryFn: async () => tags })
709
- .catch(() => { });
710
- }
711
- }
712
- catch (error) {
713
- logger$4('[download] transaction tags lookup failed', error);
714
- }
715
- }
716
- const out = {};
717
- for (const [transactionId, tags] of tagsById) {
718
- const value = tags.find((tag) => tag.name === CONTENT_SHA256_TAG)?.value?.trim();
719
- if (value)
720
- out[transactionId] = value;
721
- }
722
- return out;
723
- };
724
- /** Store the (now verified) Content-SHA-256 of each downloaded transaction on its metadata row. */
725
- const recordVerifiedContentHashes = async (transactionIds, expectedContentHashes) => {
726
- for (const transactionId of transactionIds) {
727
- const contentHash = expectedContentHashes[transactionId];
728
- if (!contentHash)
729
- continue;
730
- try {
731
- const metadataRecord = await getMetadata({ storageTransactionId: transactionId });
732
- if (metadataRecord) {
733
- await saveMetadata(metadataRecord, { contentHash });
734
- }
735
- }
736
- catch (error) {
737
- logger$4(error);
738
- }
739
- }
740
- };
741
- const downloadTransactionIds = async (rawTransactionIds, options) => {
742
- if (!supportsOpfsFileDownloads()) {
743
- return false;
744
- }
745
- const transactionIds = Array.from(new Set(rawTransactionIds.filter((tx) => !!tx && tx.trim().length > 0)));
746
- if (transactionIds.length === 0) {
747
- return false;
748
- }
749
- const excludedTransactions = await getExcludedTransactions();
750
- await ensureReadGatewaySelected();
751
- // Browser: do not call getTransactionStatus() — many gateways (e.g. g8way.io) redirect or omit
752
- // CORS headers, so fetch() fails and we would wrongly exclude txs. The OPFS worker fetch decides success.
753
- const transactionIdsToDownload = transactionIds.filter((id) => !excludedTransactions.has(id));
754
- if (transactionIdsToDownload.length === 0) {
755
- return false;
756
- }
757
- // One batched lookup for every upload's Content-SHA-256 tag; downloads are verified against it.
758
- // Missing tags (old uploads, or a just-published one not indexed yet) just mean no verification.
759
- const expectedContentHashes = await getExpectedContentHashes(transactionIdsToDownload);
760
- // Preferred gateways first: a just-published item is only on the gateway it was uploaded through.
761
- const arweaveBaseUrls = getArweaveReadBaseUrls();
762
- // Where each ItemStorage child of a composite goes (the worker cannot read the DB or the schema).
763
- const compositeChildPaths = await getCompositeChildPaths(transactionIdsToDownload).catch((error) => {
764
- logger$4('[download] composite child paths lookup failed', error);
765
- return {};
766
- });
767
- let result;
768
- if (transactionIdsToDownload.length === 1) {
769
- const transactionId = transactionIdsToDownload[0];
770
- result = await BaseFileManager.downloadFileByTransactionId({
771
- transactionId,
772
- arweaveHost: BaseArweaveClient.getBaseUrl(),
773
- arweaveBaseUrls,
774
- excludedTransactions,
775
- expectedContentHash: expectedContentHashes[transactionId],
776
- compositeChildPaths: compositeChildPaths[transactionId],
777
- });
778
- }
779
- else {
780
- result = await BaseFileManager.downloadAllFiles({
781
- transactionIds: transactionIdsToDownload,
782
- arweaveHost: BaseArweaveClient.getBaseUrl(),
783
- arweaveBaseUrls,
784
- excludedTransactions,
785
- expectedContentHashes,
786
- compositeChildPaths,
787
- });
788
- }
789
- if (result) {
790
- for (const transactionId of result.hashMismatchTransactionIds) {
791
- // Not saved by the worker; left for a later run (a gateway may serve the right bytes then).
792
- logger$4(`[download] ${transactionId}: downloaded bytes do not match Content-SHA-256; skipped`);
793
- }
794
- await recordVerifiedContentHashes(result.verifiedTransactionIds, expectedContentHashes);
795
- }
796
- if (options?.resizeAllImages !== false) {
797
- await BaseFileManager.resizeAllImages({
798
- width: 480,
799
- height: 480,
800
- });
801
- }
802
- return true;
803
- };
804
- const runBulkDownloadWithDedupe = async (transactionIds) => {
805
- if (bulkDownloadInFlight) {
806
- return bulkDownloadInFlight;
807
- }
808
- bulkDownloadInFlight = (async () => {
809
- try {
810
- await downloadTransactionIds(transactionIds, { resizeAllImages: true });
811
- }
812
- finally {
813
- bulkDownloadInFlight = null;
814
- }
815
- })();
816
- return bulkDownloadInFlight;
817
- };
818
- const downloadAllFilesRequestHandler = async ({ endpoints, eventId, }) => {
819
- if (!supportsOpfsFileDownloads()) {
820
- return;
821
- }
822
- await syncDbFiles();
823
- eventEmitter.emit('fs.downloadAll.success', { eventId });
824
- eventEmitter.emit('fs.downloadAllBinary.request', { endpoints });
825
- };
826
- const downloadAllFilesBinaryRequestHandler = async (addressesHintOrPayload) => {
827
- if (!supportsOpfsFileDownloads()) {
828
- return;
829
- }
830
- const addressesHint = Array.isArray(addressesHintOrPayload)
831
- ? addressesHintOrPayload
832
- : undefined;
833
- let fromDb = [];
834
- if (BaseDb.isAppDbReady()) {
835
- fromDb = await getAllAddressesFromDb();
836
- }
837
- if (!BaseDb.isAppDbReady()) {
838
- await getAppDbReady();
839
- fromDb = await getAllAddressesFromDb();
840
- }
841
- const addresses = mergeAddressLists(addressesHint, fromDb);
842
- if (!addresses || addresses.length === 0) {
843
- return;
844
- }
845
- const queryClient = BaseQueryClient.getQueryClient();
846
- const easClient = BaseEasClient.getEasClient();
847
- const filesMetadataQueryKey = ['getFilesMetadata', ...addresses];
848
- const fetchFilesMetadata = () => easClient.request(GET_FILES_METADATA, {
849
- where: {
850
- attester: {
851
- in: addresses,
852
- },
853
- schema: {
854
- is: {
855
- id: {
856
- equals: '0x55fdefb36fcbbaebeb7d6b41dc3a1a9666e4e42154267c889de064faa7ede517',
857
- },
858
- },
859
- },
860
- },
861
- });
862
- let { filesMetadata } = await queryClient.fetchQuery({
863
- queryKey: filesMetadataQueryKey,
864
- queryFn: fetchFilesMetadata,
865
- staleTime: GET_FILES_METADATA_STALE_MS,
866
- });
867
- if ((!Array.isArray(filesMetadata) || filesMetadata.length === 0) && addresses.length > 0) {
868
- await queryClient.removeQueries({ queryKey: filesMetadataQueryKey });
869
- ({ filesMetadata } = await queryClient.fetchQuery({
870
- queryKey: filesMetadataQueryKey,
871
- queryFn: fetchFilesMetadata,
872
- staleTime: GET_FILES_METADATA_STALE_MS,
873
- }));
874
- }
875
- const filesRoot = BaseFileManager.getWorkingDir();
876
- await BaseFileManager.createDirIfNotExists(filesRoot);
877
- await BaseFileManager.createDirIfNotExists(BaseFileManager.getFilesPath('html'));
878
- await BaseFileManager.createDirIfNotExists(BaseFileManager.getFilesPath('json'));
879
- await BaseFileManager.createDirIfNotExists(BaseFileManager.getFilesPath('images'));
880
- await BaseFileManager.createDirIfNotExists(BaseFileManager.getFilesPath('files'));
881
- const excludedTransactions = await getExcludedTransactions();
882
- const fromEas = parseTransactionIdsFromMetadata(filesMetadata ?? [], excludedTransactions);
883
- const fromLocal = await collectTransactionIdsFromLocalMetadata();
884
- const transactionIds = Array.from(new Set([...fromEas, ...fromLocal]));
885
- await runBulkDownloadWithDedupe(transactionIds);
886
- };
887
- const downloadTransactionIdWithDedupe = async (transactionId) => {
888
- if (!supportsOpfsFileDownloads() || !transactionId || transactionId.trim().length === 0) {
889
- return false;
890
- }
891
- const normalizedTxId = normalizeArweaveTxId(transactionId) ?? transactionId.trim();
892
- const existing = lazyDownloadInFlightByTransactionId.get(normalizedTxId);
893
- if (existing) {
894
- return existing;
895
- }
896
- const inFlight = (async () => {
897
- try {
898
- const ok = await downloadTransactionIds([normalizedTxId], { resizeAllImages: false });
899
- return ok;
900
- }
901
- finally {
902
- lazyDownloadInFlightByTransactionId.delete(normalizedTxId);
903
- }
904
- })();
905
- lazyDownloadInFlightByTransactionId.set(normalizedTxId, inFlight);
906
- return inFlight;
907
- };
908
- const scheduleBulkFilesDownloadFromEasSyncThrottled = throttle((addressesHint) => {
909
- void downloadAllFilesBinaryRequestHandler(addressesHint).catch((error) => {
910
- logger$4('[scheduleBulkFilesDownloadFromEasSync] failed', error);
911
- });
912
- }, AUTO_BULK_DOWNLOAD_THROTTLE_MS, { leading: true, trailing: false });
913
- const scheduleBulkFilesDownloadFromEasSync = (addressesHint) => {
914
- if (!supportsOpfsFileDownloads()) {
915
- return;
916
- }
917
- scheduleBulkFilesDownloadFromEasSyncThrottled(addressesHint);
918
- };
919
-
920
- var download = /*#__PURE__*/Object.freeze({
921
- __proto__: null,
922
- downloadAllFilesBinaryRequestHandler: downloadAllFilesBinaryRequestHandler,
923
- downloadAllFilesRequestHandler: downloadAllFilesRequestHandler,
924
- downloadTransactionIdWithDedupe: downloadTransactionIdWithDedupe,
925
- scheduleBulkFilesDownloadFromEasSync: scheduleBulkFilesDownloadFromEasSync
926
- });
927
-
928
- const logger$3 = debug('seedSdk:helpers:ensureImageLocal');
929
- /** Arweave transaction ids are 43 chars in the URL-safe base64 alphabet. */
930
- const ARWEAVE_TX_ID_RE = /^[a-z0-9_-]{43}$/i;
931
- const IMAGE_SIZES = [
932
- ImageSize.EXTRA_SMALL,
933
- ImageSize.SMALL,
934
- ImageSize.MEDIUM,
935
- ImageSize.LARGE,
936
- ImageSize.EXTRA_LARGE,
937
- ];
938
- const inFlightByKey = new Map();
939
- const normalizeArweaveTxIdForEnsure = (raw) => {
940
- if (raw == null || raw === '')
941
- return undefined;
942
- const t = String(raw).trim();
943
- if (ARWEAVE_TX_ID_RE.test(t))
944
- return t;
945
- const m = t.match(/^([a-z0-9_-]{43})(?:\.[^/]+)?$/i);
946
- if (m)
947
- return m[1];
948
- const embedded = t.match(/[a-z0-9_-]{43}/gi);
949
- if (embedded) {
950
- for (const candidate of embedded) {
951
- if (ARWEAVE_TX_ID_RE.test(candidate))
952
- return candidate;
953
- }
954
- }
955
- return undefined;
956
- };
957
- const closestImageSize = (width) => {
958
- let best = ImageSize.EXTRA_SMALL;
959
- let bestDist = Math.abs(best - width);
960
- for (const size of IMAGE_SIZES) {
961
- const dist = Math.abs(size - width);
962
- if (dist < bestDist) {
963
- best = size;
964
- bestDist = dist;
965
- }
966
- }
967
- return best;
968
- };
969
- const mapRequestedWidthsToImageSizes = (widths) => {
970
- const requested = widths && widths.length > 0 ? widths : [ImageSize.EXTRA_SMALL];
971
- const unique = new Set();
972
- for (const w of requested) {
973
- if (typeof w !== 'number' || !Number.isFinite(w) || w <= 0)
974
- continue;
975
- unique.add(closestImageSize(w));
976
- }
977
- if (unique.size === 0) {
978
- unique.add(ImageSize.EXTRA_SMALL);
979
- }
980
- return Array.from(unique).sort((a, b) => a - b);
981
- };
982
- const getBasename = (filePath) => {
983
- const path = BaseFileManager.getPathModule();
984
- return path.basename(filePath);
985
- };
986
- const getBasenameWithoutExtension = (fileName) => {
987
- const match = fileName.match(/^(.*[\/\\])?([^\/\\]+?)(\.[^.\/\\]*)?$/);
988
- return match?.[2] ?? fileName;
989
- };
990
- const resolveMatchingImagePath = async (needle) => {
991
- const dirPath = BaseFileManager.getFilesPath('images');
992
- const dirExists = await BaseFileManager.pathExists(dirPath);
993
- if (!dirExists)
994
- return undefined;
995
- const fs = await BaseFileManager.getFs();
996
- const path = BaseFileManager.getPathModule();
997
- const files = await fs.promises.readdir(dirPath);
998
- // Prefer exact basename match (files only); skip size dirs.
999
- const exact = files.find((file) => path.basename(file) === needle);
1000
- if (exact) {
1001
- const candidate = BaseFileManager.getFilesPath('images', exact);
1002
- try {
1003
- const zenfs = await BaseFileManager.getFs();
1004
- const stat = await zenfs.promises.stat(candidate);
1005
- if (stat.isFile())
1006
- return candidate;
1007
- }
1008
- catch {
1009
- // fall through
1010
- }
1011
- }
1012
- const matchingFiles = files.filter((file) => {
1013
- const base = path.basename(file);
1014
- return base.includes(needle);
1015
- });
1016
- for (const file of matchingFiles) {
1017
- const candidate = BaseFileManager.getFilesPath('images', file);
1018
- try {
1019
- const zenfs = await BaseFileManager.getFs();
1020
- const stat = await zenfs.promises.stat(candidate);
1021
- if (stat.isFile())
1022
- return candidate;
1023
- }
1024
- catch {
1025
- continue;
1026
- }
1027
- }
1028
- return undefined;
1029
- };
1030
- const sizedVariantPath = (originalFilePath, size) => {
1031
- const path = BaseFileManager.getPathModule();
1032
- const dir = path.dirname(originalFilePath);
1033
- const base = getBasenameWithoutExtension(getBasename(originalFilePath));
1034
- return path.join(dir, String(size), `${base}.webp`);
1035
- };
1036
- const ensureKey = (txId, fileName, sizes) => {
1037
- const idPart = txId ?? fileName ?? '';
1038
- return `${idPart}|${sizes.join(',')}`;
1039
- };
1040
- const runEnsureImageLocal = async (params) => {
1041
- if (!supportsOpfsFileDownloads()) {
1042
- return { status: 'unsupported' };
1043
- }
1044
- const fileName = params.fileName?.trim() || undefined;
1045
- const txId = normalizeArweaveTxIdForEnsure(params.transactionId) ??
1046
- normalizeArweaveTxIdForEnsure(fileName);
1047
- if (!txId && !fileName) {
1048
- return { status: 'missing-id' };
1049
- }
1050
- const targetSizes = mapRequestedWidthsToImageSizes(params.widths);
1051
- const needles = [fileName, txId].filter((n) => typeof n === 'string' && n.length > 0);
1052
- try {
1053
- let filePath;
1054
- for (const needle of needles) {
1055
- filePath = await resolveMatchingImagePath(needle);
1056
- if (filePath)
1057
- break;
1058
- }
1059
- if (!filePath && txId) {
1060
- await downloadTransactionIdWithDedupe(txId);
1061
- for (const needle of needles) {
1062
- filePath = await resolveMatchingImagePath(needle);
1063
- if (filePath)
1064
- break;
1065
- }
1066
- // Downloads store as bare tx id
1067
- if (!filePath) {
1068
- const direct = BaseFileManager.getFilesPath('images', txId);
1069
- if (await BaseFileManager.pathExists(direct)) {
1070
- filePath = direct;
1071
- }
1072
- }
1073
- }
1074
- if (!filePath) {
1075
- return { status: 'failed' };
1076
- }
1077
- const createdWidths = [];
1078
- for (const size of targetSizes) {
1079
- const variantPath = sizedVariantPath(filePath, size);
1080
- const exists = await BaseFileManager.pathExists(variantPath);
1081
- if (exists)
1082
- continue;
1083
- await BaseFileManager.resizeImage({
1084
- filePath,
1085
- width: size,
1086
- height: size,
1087
- });
1088
- createdWidths.push(size);
1089
- }
1090
- return {
1091
- status: 'ready',
1092
- filePath,
1093
- createdWidths,
1094
- };
1095
- }
1096
- catch (error) {
1097
- logger$3('ensureImageLocal failed', error);
1098
- return { status: 'failed' };
1099
- }
1100
- };
1101
- /**
1102
- * Ensure an image original exists in OPFS (downloading from Arweave if needed)
1103
- * and create only the requested ImageSize variant(s) for that one file.
1104
- */
1105
- const ensureImageLocal = async (params) => {
1106
- if (!supportsOpfsFileDownloads()) {
1107
- return { status: 'unsupported' };
1108
- }
1109
- const fileName = params.fileName?.trim() || undefined;
1110
- const txId = normalizeArweaveTxIdForEnsure(params.transactionId) ??
1111
- normalizeArweaveTxIdForEnsure(fileName);
1112
- if (!txId && !fileName) {
1113
- return { status: 'missing-id' };
1114
- }
1115
- const targetSizes = mapRequestedWidthsToImageSizes(params.widths);
1116
- const key = ensureKey(txId, fileName, targetSizes);
1117
- const existing = inFlightByKey.get(key);
1118
- if (existing) {
1119
- return existing;
1120
- }
1121
- const inFlight = (async () => {
1122
- try {
1123
- return await runEnsureImageLocal({
1124
- transactionId: txId,
1125
- fileName,
1126
- widths: targetSizes,
1127
- });
1128
- }
1129
- finally {
1130
- inFlightByKey.delete(key);
1131
- }
1132
- })();
1133
- inFlightByKey.set(key, inFlight);
1134
- return inFlight;
1135
- };
1136
-
1137
- const randomId = customAlphabet(alphanumeric, 10);
1138
- /**
1139
- * A 10-character local id. Never starts with "0x" (about 1 in 3,844 random ids would), so code that
1140
- * tells local ids from EAS uids by that prefix can't mistake one for the other.
1141
- */
1142
- const generateId = () => {
1143
- let id = randomId();
1144
- while (id.startsWith('0x'))
1145
- id = randomId();
1146
- return id;
1147
- };
1148
-
1149
- var generateId$1 = /*#__PURE__*/Object.freeze({
1150
- __proto__: null,
1151
- generateId: generateId
1152
- });
1153
-
1154
- const logger$2 = debug('seedSdk:shared:helpers');
1155
- const toSnakeCase = (str) => {
1156
- return str.replace(/([a-z])([A-Z])/g, '$1_$2').toLowerCase();
1157
- };
1158
- const getMimeType = (base64) => {
1159
- if (!base64) {
1160
- return null;
1161
- }
1162
- const result = base64.match(/^data:([a-zA-Z0-9]+\/[a-zA-Z0-9-.+]+).*,/);
1163
- if (result && result.length > 1) {
1164
- return result[1];
1165
- }
1166
- else {
1167
- return null; // MIME type could not be determined
1168
- }
1169
- };
1170
- const getCorrectId = (localIdOrUid) => {
1171
- const id = {
1172
- localId: undefined,
1173
- uid: undefined,
1174
- };
1175
- if (!localIdOrUid) {
1176
- return id;
1177
- }
1178
- if (localIdOrUid.length === 10) {
1179
- id.localId = localIdOrUid;
1180
- }
1181
- if (localIdOrUid.startsWith('0x') && localIdOrUid.length === 66) {
1182
- id.uid = localIdOrUid;
1183
- }
1184
- return id;
1185
- };
1186
- const getDataTypeFromString = (data) => {
1187
- const nonImageBase64Regex = /^(?!data:image\/(?:jpeg|png|gif|bmp|webp);base64,)[A-Za-z0-9+/=]+$/;
1188
- if (nonImageBase64Regex.test(data)) {
1189
- return 'base64';
1190
- }
1191
- // Regular expression for base64 (simple version, checking for base64 format)
1192
- const imageBase64Regex = /^data:image\/[a-zA-Z]+;base64,[A-Za-z0-9+/]+={0,2}$/;
1193
- if (imageBase64Regex.test(data)) {
1194
- return 'imageBase64';
1195
- }
1196
- // Blob URLs are fetchable but session-scoped; recognize them as url type
1197
- if (data.startsWith('blob:')) {
1198
- return 'url';
1199
- }
1200
- // Regular expression for URL (simple version, checking for common URL format)
1201
- const urlRegex = /^(http:\/\/www\.|https:\/\/www\.|http:\/\/|https:\/\/)?[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,5}(:[0-9]{1,5})?(\/.*)?$/;
1202
- if (urlRegex.test(data)) {
1203
- return 'url';
1204
- }
1205
- return null;
1206
- };
1207
- const convertTxIdToImage = async (txId) => {
1208
- const imageFilePath = BaseFileManager.getFilesPath('images', txId);
1209
- const fileExists = await BaseFileManager.pathExists(imageFilePath);
1210
- if (!fileExists) {
1211
- logger$2(`[ItemView] [updateImage] ${imageFilePath} does not exist`);
1212
- return;
1213
- }
1214
- const buffer = await BaseFileManager.readFileAsBuffer(imageFilePath);
1215
- // Convert Blob to ArrayBuffer if needed, or use Buffer directly
1216
- let arrayBuffer;
1217
- if (buffer instanceof Blob) {
1218
- arrayBuffer = await buffer.arrayBuffer();
1219
- }
1220
- else if (buffer instanceof ArrayBuffer) {
1221
- arrayBuffer = buffer;
1222
- }
1223
- else {
1224
- // Node.js Buffer
1225
- const sliced = buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
1226
- // Handle SharedArrayBuffer case by creating a new ArrayBuffer
1227
- if (sliced instanceof SharedArrayBuffer) {
1228
- const uint8Array = new Uint8Array(sliced);
1229
- arrayBuffer = uint8Array.buffer.slice(0);
1230
- }
1231
- else {
1232
- arrayBuffer = sliced;
1233
- }
1234
- }
1235
- const uint = new Uint8Array(arrayBuffer);
1236
- const imageBlob = new Blob([uint]);
1237
- return URL.createObjectURL(imageBlob);
1238
- };
1239
- /**
1240
- * Constructs an Arweave URL for a storage transaction ID
1241
- * @param storageTransactionId - The Arweave transaction ID
1242
- * @returns The full URL to access the transaction data on Arweave (e.g., https://arweave.net/raw/{transactionId})
1243
- * @deprecated Use BaseArweaveClient.getRawUrl() instead for better consistency and testability
1244
- */
1245
- const getArweaveUrlForTransaction = (storageTransactionId) => {
1246
- return BaseArweaveClient.getRawUrl(storageTransactionId);
1247
- };
1248
-
1249
- /** Emitted by the client manager after `app_state.addresses` is written (post-`setAddresses`). */
1250
- const ADDRESSES_PERSISTED_EVENT = 'addresses.persisted';
1251
- /** Emitted after hard-deleting local on-chain seed copies for publisher addresses. */
1252
- const LOCAL_COPIES_REMOVED_EVENT = 'localCopies.removed';
1253
- function parseAddressesPersistedPayload(value) {
1254
- if (!value || typeof value !== 'object') {
1255
- return { owned: [], watched: [] };
1256
- }
1257
- const v = value;
1258
- const owned = Array.isArray(v.owned)
1259
- ? v.owned.filter((a) => typeof a === 'string')
1260
- : [];
1261
- const watched = Array.isArray(v.watched)
1262
- ? v.watched.filter((a) => typeof a === 'string')
1263
- : [];
1264
- return { owned, watched };
1265
- }
1266
-
1267
- /**
1268
- * Cross-tab exclusive sections, using the Web Locks API. All tabs of an origin share one SQLite
1269
- * database and OPFS tree; the browser releases a tab's locks when it closes or crashes, so a lock
1270
- * can't go stale. See docs/MULTI_TAB.md.
1271
- *
1272
- * Node has Web Locks from 24.5, scoped to the process. Where they're missing (older Node, old
1273
- * browsers), sections still run one at a time within this JS realm (see `withProcessLock`), which
1274
- * covers a single Node process but not other tabs or worker threads.
1275
- * Not re-entrant: requesting a name the caller already holds waits forever, so take each lock
1276
- * only at the outermost call site.
1277
- */
1278
- /** Long enough for a slow init in another tab; short enough that a hung tab doesn't block forever. */
1279
- const TAB_LOCK_WAIT_TIMEOUT_MS = 60000;
1280
- function seedDbLockName(scope, filesDir) {
1281
- return `seed:${scope}:${filesDir}/db/seed.db`;
1282
- }
1283
- /** `withTabLock` on the database under `filesDir`; runs `fn` directly when there's no `filesDir` yet. */
1284
- function withSeedDbLock(scope, filesDir, fn) {
1285
- return filesDir ? withTabLock(seedDbLockName(scope, filesDir), fn) : fn();
1286
- }
1287
- class TabLockTimeoutError extends Error {
1288
- constructor(lockName, timeoutMs) {
1289
- super(`Timed out after ${timeoutMs}ms waiting for another tab to release "${lockName}".`);
1290
- this.lockName = lockName;
1291
- this.code = 'TAB_LOCK_TIMEOUT';
1292
- this.name = 'TabLockTimeoutError';
1293
- }
1294
- }
1295
- async function withTabLock(name, fn, { timeoutMs = TAB_LOCK_WAIT_TIMEOUT_MS } = {}) {
1296
- const locks = typeof navigator !== 'undefined' ? navigator.locks : undefined;
1297
- if (!locks)
1298
- return withProcessLock(name, fn, { timeoutMs });
1299
- try {
1300
- // The timeout bounds the wait only; once granted, fn runs to completion. Infinity waits for as
1301
- // long as the holder runs (it can't outlive its tab).
1302
- const signal = Number.isFinite(timeoutMs) ? AbortSignal.timeout(timeoutMs) : undefined;
1303
- return await locks.request(name, { mode: 'exclusive', signal }, fn);
1304
- }
1305
- catch (error) {
1306
- if (error?.name === 'TimeoutError') {
1307
- throw new TabLockTimeoutError(name, timeoutMs);
1308
- }
1309
- throw error;
1310
- }
1311
- }
1312
- /** Tail of each name's queue: resolves once every section queued so far has finished or given up. */
1313
- const processLockTails = new Map();
1314
- /**
1315
- * The `withTabLock` fallback without Web Locks: an exclusive, first-come-first-served lock per name
1316
- * within this JS realm. Like Web Locks, `timeoutMs` bounds the wait only, and a waiter that times
1317
- * out doesn't let later waiters in before the current holder finishes.
1318
- */
1319
- async function withProcessLock(name, fn, { timeoutMs = TAB_LOCK_WAIT_TIMEOUT_MS } = {}) {
1320
- const previous = processLockTails.get(name) ?? Promise.resolve();
1321
- let release;
1322
- const done = new Promise((resolve) => (release = resolve));
1323
- const tail = previous.then(() => done);
1324
- processLockTails.set(name, tail);
1325
- void tail.then(() => {
1326
- if (processLockTails.get(name) === tail)
1327
- processLockTails.delete(name);
1328
- });
1329
- try {
1330
- await waitForTurn(previous, name, timeoutMs);
1331
- }
1332
- catch (error) {
1333
- release();
1334
- throw error;
1335
- }
1336
- try {
1337
- return await fn();
1338
- }
1339
- finally {
1340
- release();
1341
- }
1342
- }
1343
- function waitForTurn(previous, name, timeoutMs) {
1344
- if (!Number.isFinite(timeoutMs))
1345
- return previous;
1346
- let timer;
1347
- const timedOut = new Promise((_, reject) => {
1348
- timer = setTimeout(() => reject(new TabLockTimeoutError(name, timeoutMs)), timeoutMs);
1349
- });
1350
- return Promise.race([previous, timedOut]).finally(() => clearTimeout(timer));
1351
- }
1352
-
1353
- const logger$1 = debug('seedSdk:helpers:tabCoordinator');
1354
- /** Identifies this tab (this page's SDK instance) in the messages it sends. */
1355
- const TAB_ID = typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function'
1356
- ? crypto.randomUUID()
1357
- : `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
1358
- let state;
1359
- let leader = false;
1360
- let leaderWaiters = [];
1361
- const messageHandlers = new Set();
1362
- /** Starts once per page; later calls (re-init) keep the first database. */
1363
- function startTabCoordination({ filesDir, mode = 'coordinate' }) {
1364
- if (state)
1365
- return;
1366
- const locks = typeof navigator !== 'undefined' ? navigator.locks : undefined;
1367
- const dbKey = `${filesDir}/db/seed.db`;
1368
- const coordinating = mode === 'coordinate' && isBrowser() && !!locks && typeof BroadcastChannel !== 'undefined';
1369
- state = { dbKey, coordinating };
1370
- if (!coordinating) {
1371
- becomeLeader();
1372
- return;
1373
- }
1374
- const channel = new BroadcastChannel(`seed:tabs:${dbKey}`);
1375
- channel.onmessage = (event) => {
1376
- // A BroadcastChannel never hears itself, but another channel object in this page would (a
1377
- // second channel opened by a test, or the same module loaded twice): skip our own messages.
1378
- if (event.data?.from === TAB_ID)
1379
- return;
1380
- for (const handler of messageHandlers)
1381
- handler(event.data);
1382
- };
1383
- state.channel = channel;
1384
- const current = state;
1385
- void locks.request(`seed:leader:${dbKey}`, () => {
1386
- // Reset (tests) before the grant: hand the lock straight back.
1387
- if (state !== current)
1388
- return undefined;
1389
- becomeLeader();
1390
- // Held until the tab closes (or a test resets coordination).
1391
- return new Promise((release) => {
1392
- current.releaseLeadership = release;
1393
- });
1394
- });
1395
- }
1396
- function becomeLeader() {
1397
- if (leader)
1398
- return;
1399
- leader = true;
1400
- logger$1(`this tab leads ${state?.dbKey}`);
1401
- const waiters = leaderWaiters;
1402
- leaderWaiters = [];
1403
- for (const resolve of waiters)
1404
- resolve();
1405
- }
1406
- function isLeaderTab() {
1407
- return !state || leader;
1408
- }
1409
- /** Resolves when this tab leads (at once if it already does). */
1410
- function whenLeaderTab() {
1411
- if (isLeaderTab())
1412
- return Promise.resolve();
1413
- return new Promise((resolve) => leaderWaiters.push(resolve));
1414
- }
1415
- /** Sends to the other tabs of this database; a no-op when not coordinating. */
1416
- function postTabMessage(message) {
1417
- state?.channel?.postMessage({ ...message, from: TAB_ID });
1418
- }
1419
- /** Whether postTabMessage reaches other tabs (a browser tab coordinating with Web Locks). */
1420
- function isPostingTabMessages() {
1421
- return !!state?.channel;
1422
- }
1423
- function onTabMessage(handler) {
1424
- messageHandlers.add(handler);
1425
- return () => messageHandlers.delete(handler);
1426
- }
1427
- /** `withTabLock` named for this database (`seed:<scope>:<db>`); runs `fn` directly before init. */
1428
- function withDbTabLock(scope, fn, options) {
1429
- return state ? withTabLock(`seed:${scope}:${state.dbKey}`, fn, options) : fn();
1430
- }
1431
-
1432
- const logger = debug('seedSdk:helpers:tabEvents');
1433
- /**
1434
- * Keeps other tabs current (docs/MULTI_TAB.md). The event bus is per tab, so events that mean "the
1435
- * shared database or OPFS changed" are also posted to the other tabs, which refresh their caches
1436
- * and emit the same event locally. Events are posted only where they originate, so a relayed event
1437
- * is never relayed again.
1438
- */
1439
- /** Emitted with the changed paths when files change outside this tab's file-system cache. */
1440
- const FILES_CHANGED_EVENT = 'fs.files.changed';
1441
- /** Emits on this tab's event bus and in every other tab of this database. */
1442
- function emitAcrossTabs(name, payload) {
1443
- if (payload === undefined) {
1444
- eventEmitter.emit(name);
1445
- }
1446
- else {
1447
- eventEmitter.emit(name, payload);
1448
- }
1449
- postTabMessage({ type: 'event', name, payload });
1450
- }
1451
- /**
1452
- * A file saved through this tab's file system: emits `file-saved` here. Other tabs hear about it
1453
- * from the file system itself (see queueFilesChangedNotice), once the write has reached OPFS.
1454
- */
1455
- function notifyFileSaved(filePath) {
1456
- eventEmitter.emit('file-saved', filePath);
1457
- }
1458
- /**
1459
- * Files written straight to OPFS by a worker: refreshes this tab's file-system cache, emits
1460
- * FILES_CHANGED_EVENT, and tells other tabs to do the same.
1461
- */
1462
- async function notifyFilesWrittenOutsideCache(paths) {
1463
- if (paths.length === 0)
1464
- return;
1465
- await BaseFileManager.invalidateCachedPaths(paths);
1466
- eventEmitter.emit(FILES_CHANGED_EVENT, paths);
1467
- queueFilesChangedNotice(paths);
1468
- }
1469
- /**
1470
- * How long changed paths gather before one `files-changed` message goes out. Counted from the
1471
- * first queued path, so a steady stream of writes still sends once per window, never later.
1472
- */
1473
- const FILES_CHANGED_FLUSH_MS = 25;
1474
- /** Keeps a message for a huge batch (a bulk download) from being one giant structured clone. */
1475
- const FILES_CHANGED_MAX_PATHS = 500;
1476
- let pendingChangedPaths = new Set();
1477
- let flushTimer;
1478
- let flushOnPageHide = false;
1479
- /**
1480
- * Tells other tabs that these absolute ZenFS paths changed in OPFS, so they drop and re-read their
1481
- * cached copies (the sync API's in-memory copy in particular). The browser file system calls this
1482
- * for every file it writes, truncates or removes (`TolerantWebAccessFS.onOpfsChange`, wired in
1483
- * `BrowserFileManager.initializeFileSystem`), after the change reached OPFS. Paths are coalesced
1484
- * for FILES_CHANGED_FLUSH_MS, so bulk writes (migrations, schema processing, downloads) send a few
1485
- * messages rather than one per write.
1486
- *
1487
- * Defense in depth only: a tab that misses a message still sees the change on its next async
1488
- * stat() (see TolerantWebAccessFS.stat). A no-op unless this tab coordinates with others.
1489
- */
1490
- function queueFilesChangedNotice(paths) {
1491
- if (!isPostingTabMessages())
1492
- return;
1493
- for (const path of paths)
1494
- pendingChangedPaths.add(path);
1495
- if (flushTimer !== undefined || pendingChangedPaths.size === 0)
1496
- return;
1497
- flushTimer = setTimeout(flushFilesChangedNotices, FILES_CHANGED_FLUSH_MS);
1498
- if (!flushOnPageHide && typeof addEventListener === 'function') {
1499
- flushOnPageHide = true;
1500
- // A tab closing inside the window still sends what it wrote.
1501
- addEventListener('pagehide', flushFilesChangedNotices);
1502
- }
1503
- }
1504
- /** Sends the queued paths now (page hide, tests). */
1505
- function flushFilesChangedNotices() {
1506
- if (flushTimer !== undefined)
1507
- clearTimeout(flushTimer);
1508
- flushTimer = undefined;
1509
- if (pendingChangedPaths.size === 0)
1510
- return;
1511
- const paths = [...pendingChangedPaths];
1512
- pendingChangedPaths = new Set();
1513
- for (let i = 0; i < paths.length; i += FILES_CHANGED_MAX_PATHS) {
1514
- postTabMessage({ type: 'files-changed', paths: paths.slice(i, i + FILES_CHANGED_MAX_PATHS) });
1515
- }
1516
- }
1517
- /** Refreshes this tab's in-memory state for an event another tab emitted, before re-emitting it. */
1518
- async function catchUpOn(name, payload) {
1519
- if (name === EAS_SEED_DATA_SYNCED_TO_DB_EVENT) {
1520
- const _mod_1 = await import('./Item-C7sUUu2B.js');
1521
- const _ns_1 = _mod_1.a2;
1522
- const { Item } = _ns_1;
1523
- await Item.rehydrateCachedItemsFromDbAfterEasSync();
1524
- }
1525
- else if (name === LOCAL_COPIES_REMOVED_EVENT) {
1526
- const { removedSeedLocalIds = [], removedSeedUids = [] } = (payload ?? {});
1527
- const _mod_2 = await import('./Item-C7sUUu2B.js');
1528
- const _ns_2 = _mod_2.a2;
1529
- const { Item } = _ns_2;
1530
- Item.dropCachedInstancesForSeedIds([...removedSeedLocalIds, ...removedSeedUids]);
1531
- }
1532
- }
1533
- const RELAYED_EVENTS = new Set([
1534
- EAS_SEED_DATA_SYNCED_TO_DB_EVENT,
1535
- ADDRESSES_PERSISTED_EVENT,
1536
- LOCAL_COPIES_REMOVED_EVENT,
1537
- ]);
1538
- let listening = false;
1539
- /** Applies other tabs' events and file changes in this tab. */
1540
- function listenForOtherTabsEvents() {
1541
- if (listening)
1542
- return;
1543
- listening = true;
1544
- onTabMessage((message) => {
1545
- if (message.type === 'event' && RELAYED_EVENTS.has(message.name)) {
1546
- void catchUpOn(message.name, message.payload)
1547
- .catch((error) => logger(`catching up on ${message.name} failed`, error))
1548
- .then(() => {
1549
- if (message.payload === undefined) {
1550
- eventEmitter.emit(message.name);
1551
- }
1552
- else {
1553
- eventEmitter.emit(message.name, message.payload);
1554
- }
1555
- });
1556
- }
1557
- else if (message.type === 'files-changed') {
1558
- // Only refreshes caches: never writes, so never sends files-changed back.
1559
- void BaseFileManager.invalidateCachedPaths(message.paths)
1560
- .catch((error) => logger('refreshing changed files failed', error))
1561
- .then(() => eventEmitter.emit(FILES_CHANGED_EVENT, message.paths));
1562
- }
1563
- });
1564
- }
1565
-
1566
- var tabEvents = /*#__PURE__*/Object.freeze({
1567
- __proto__: null,
1568
- FILES_CHANGED_EVENT: FILES_CHANGED_EVENT,
1569
- FILES_CHANGED_FLUSH_MS: FILES_CHANGED_FLUSH_MS,
1570
- FILES_CHANGED_MAX_PATHS: FILES_CHANGED_MAX_PATHS,
1571
- emitAcrossTabs: emitAcrossTabs,
1572
- flushFilesChangedNotices: flushFilesChangedNotices,
1573
- listenForOtherTabsEvents: listenForOtherTabsEvents,
1574
- notifyFileSaved: notifyFileSaved,
1575
- notifyFilesWrittenOutsideCache: notifyFilesWrittenOutsideCache,
1576
- queueFilesChangedNotice: queueFilesChangedNotice
1577
- });
1578
-
1579
- export { ADDRESSES_PERSISTED_EVENT as A, whenLeaderTab as B, CONTENT_SHA256_TAG as C, withDbTabLock as D, notifyFilesWrittenOutsideCache as E, FILES_CHANGED_EVENT as F, withSeedDbLock as G, postTabMessage as H, onTabMessage as I, listenForOtherTabsEvents as J, isNode as K, LOCAL_COPIES_REMOVED_EVENT as L, isBrowser as M, startTabCoordination as N, downloadAllFilesRequestHandler as O, downloadAllFilesBinaryRequestHandler as P, getStorageTransactionIdForSeedUid as Q, convertTxIdToImage as R, downloadTransactionIdWithDedupe as S, TabLockTimeoutError as T, getDataTypeFromString as U, getMimeType as V, notifyFileSaved as W, withTabLock as X, download as Y, generateId$1 as Z, tabEvents as _, getItemStorageSubdir as a, buildArweaveContentTags as b, getItemStorageLocalFileName as c, getContentSha256 as d, getCorrectId as e, isStorageSeedRef as f, getStorageDirForDataType as g, emitAcrossTabs as h, isPublishedSeedRef as i, generateId as j, getArweaveUrlForTransaction as k, CONTENT_TYPE_TAG as l, classifyMediaRef as m, normalizeRelationPropertyValue as n, closestImageSize as o, contentSha256TagMatches as p, ensureImageLocal as q, resolveSeedIdsFromRefString as r, scheduleBulkFilesDownloadFromEasSync as s, toSnakeCase as t, getFeedItemStringField as u, isLeaderTab as v, normalizeArweaveTxIdForEnsure as w, normalizeFeedItemFields as x, parseAddressesPersistedPayload as y, resolveMediaRef as z };
1580
- //# sourceMappingURL=tabEvents-DguZpCCv.js.map