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