emdash 0.32.0 → 0.34.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{adapters-C0ONzkUP.d.mts → adapters-34EN2eXO.d.mts} +34 -2
- package/dist/adapters-34EN2eXO.d.mts.map +1 -0
- package/dist/after-CBU2vkoO.mjs +96 -0
- package/dist/after-CBU2vkoO.mjs.map +1 -0
- package/dist/{allowed-origins-CCEi9bPI.mjs → allowed-origins-DW7sYyqg.mjs} +2 -2
- package/dist/{allowed-origins-CCEi9bPI.mjs.map → allowed-origins-DW7sYyqg.mjs.map} +1 -1
- package/dist/api/route-utils.d.mts +3 -4
- package/dist/api/route-utils.d.mts.map +1 -1
- package/dist/api/route-utils.mjs +26 -25
- package/dist/api/route-utils.mjs.map +1 -1
- package/dist/api/schemas/index.d.mts +2 -2
- package/dist/api/schemas/index.mjs +6 -5
- package/dist/{api-DgPbMlng.mjs → api-DdAsirBe.mjs} +321 -428
- package/dist/api-DdAsirBe.mjs.map +1 -0
- package/dist/{api-tokens-p-iMzvXR.mjs → api-tokens-CFXu8F_2.mjs} +2 -2
- package/dist/{api-tokens-p-iMzvXR.mjs.map → api-tokens-CFXu8F_2.mjs.map} +1 -1
- package/dist/{apply-1_6ra7NP.mjs → apply-D5zfeD09.mjs} +139 -102
- package/dist/apply-D5zfeD09.mjs.map +1 -0
- package/dist/astro/index.d.mts +12 -12
- package/dist/astro/index.d.mts.map +1 -1
- package/dist/astro/index.mjs +103 -35
- package/dist/astro/index.mjs.map +1 -1
- package/dist/astro/middleware/auth.d.mts +10 -10
- package/dist/astro/middleware/auth.mjs +9 -9
- package/dist/astro/middleware/media-usage-write-fence.d.mts +7 -0
- package/dist/astro/middleware/media-usage-write-fence.d.mts.map +1 -0
- package/dist/astro/middleware/media-usage-write-fence.mjs +35 -0
- package/dist/astro/middleware/media-usage-write-fence.mjs.map +1 -0
- package/dist/astro/middleware/redirect.d.mts.map +1 -1
- package/dist/astro/middleware/redirect.mjs +13 -10
- package/dist/astro/middleware/redirect.mjs.map +1 -1
- package/dist/astro/middleware/request-context.mjs +5 -5
- package/dist/astro/middleware/setup.mjs +1 -1
- package/dist/astro/middleware.d.mts +12 -11
- package/dist/astro/middleware.d.mts.map +1 -1
- package/dist/astro/middleware.mjs +1915 -223
- package/dist/astro/middleware.mjs.map +1 -1
- package/dist/astro/routes/api/admin/allowed-domains/_domain_.mjs +9 -8
- package/dist/astro/routes/api/admin/allowed-domains/_domain_.mjs.map +1 -1
- package/dist/astro/routes/api/admin/allowed-domains/index.mjs +9 -8
- package/dist/astro/routes/api/admin/allowed-domains/index.mjs.map +1 -1
- package/dist/astro/routes/api/admin/api-tokens/_id_.mjs +5 -5
- package/dist/astro/routes/api/admin/api-tokens/index.mjs +6 -6
- package/dist/astro/routes/api/admin/byline-fields/_slug_/usage.mjs +6 -6
- package/dist/astro/routes/api/admin/byline-fields/_slug_.mjs +12 -11
- package/dist/astro/routes/api/admin/byline-fields/_slug_.mjs.map +1 -1
- package/dist/astro/routes/api/admin/byline-fields/index.mjs +12 -11
- package/dist/astro/routes/api/admin/byline-fields/index.mjs.map +1 -1
- package/dist/astro/routes/api/admin/byline-fields/reorder.mjs +12 -11
- package/dist/astro/routes/api/admin/byline-fields/reorder.mjs.map +1 -1
- package/dist/astro/routes/api/admin/bylines/_id_/index.mjs +20 -20
- package/dist/astro/routes/api/admin/bylines/_id_/translations.mjs +20 -20
- package/dist/astro/routes/api/admin/bylines/index.mjs +20 -20
- package/dist/astro/routes/api/admin/comments/_id_/status.mjs +18 -17
- package/dist/astro/routes/api/admin/comments/_id_/status.mjs.map +1 -1
- package/dist/astro/routes/api/admin/comments/_id_.mjs +8 -8
- package/dist/astro/routes/api/admin/comments/bulk.mjs +14 -13
- package/dist/astro/routes/api/admin/comments/bulk.mjs.map +1 -1
- package/dist/astro/routes/api/admin/comments/counts.mjs +8 -8
- package/dist/astro/routes/api/admin/comments/index.mjs +14 -13
- package/dist/astro/routes/api/admin/comments/index.mjs.map +1 -1
- package/dist/astro/routes/api/admin/hooks/exclusive/_hookName_.mjs +6 -6
- package/dist/astro/routes/api/admin/hooks/exclusive/index.mjs +5 -5
- package/dist/astro/routes/api/admin/media-usage/collection-deletions/index.d.mts +8 -0
- package/dist/astro/routes/api/admin/media-usage/collection-deletions/index.d.mts.map +1 -0
- package/dist/astro/routes/api/admin/media-usage/collection-deletions/index.mjs +36 -0
- package/dist/astro/routes/api/admin/media-usage/collection-deletions/index.mjs.map +1 -0
- package/dist/astro/routes/api/admin/media-usage/collection-deletions/retry.d.mts +8 -0
- package/dist/astro/routes/api/admin/media-usage/collection-deletions/retry.d.mts.map +1 -0
- package/dist/astro/routes/api/admin/media-usage/collection-deletions/retry.mjs +36 -0
- package/dist/astro/routes/api/admin/media-usage/collection-deletions/retry.mjs.map +1 -0
- package/dist/astro/routes/api/admin/media-usage/repair.mjs +15 -14
- package/dist/astro/routes/api/admin/media-usage/repair.mjs.map +1 -1
- package/dist/astro/routes/api/admin/media-usage/work/index.d.mts +8 -0
- package/dist/astro/routes/api/admin/media-usage/work/index.d.mts.map +1 -0
- package/dist/astro/routes/api/admin/media-usage/work/index.mjs +37 -0
- package/dist/astro/routes/api/admin/media-usage/work/index.mjs.map +1 -0
- package/dist/astro/routes/api/admin/media-usage/work/retry.d.mts +8 -0
- package/dist/astro/routes/api/admin/media-usage/work/retry.d.mts.map +1 -0
- package/dist/astro/routes/api/admin/media-usage/work/retry.mjs +37 -0
- package/dist/astro/routes/api/admin/media-usage/work/retry.mjs.map +1 -0
- package/dist/astro/routes/api/admin/oauth-clients/_id_.mjs +5 -5
- package/dist/astro/routes/api/admin/oauth-clients/index.mjs +5 -5
- package/dist/astro/routes/api/admin/plugins/_id_/disable.d.mts.map +1 -1
- package/dist/astro/routes/api/admin/plugins/_id_/disable.mjs +56 -47
- package/dist/astro/routes/api/admin/plugins/_id_/disable.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/_id_/enable.d.mts.map +1 -1
- package/dist/astro/routes/api/admin/plugins/_id_/enable.mjs +56 -47
- package/dist/astro/routes/api/admin/plugins/_id_/enable.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/_id_/index.mjs +52 -46
- package/dist/astro/routes/api/admin/plugins/_id_/index.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/_id_/mcp.mjs +6 -6
- package/dist/astro/routes/api/admin/plugins/_id_/settings.mjs +52 -46
- package/dist/astro/routes/api/admin/plugins/_id_/settings.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/_id_/uninstall.d.mts.map +1 -1
- package/dist/astro/routes/api/admin/plugins/_id_/uninstall.mjs +55 -46
- package/dist/astro/routes/api/admin/plugins/_id_/uninstall.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/_id_/update.d.mts.map +1 -1
- package/dist/astro/routes/api/admin/plugins/_id_/update.mjs +55 -46
- package/dist/astro/routes/api/admin/plugins/_id_/update.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/index.mjs +52 -46
- package/dist/astro/routes/api/admin/plugins/index.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/marketplace/_id_/icon.mjs +4 -4
- package/dist/astro/routes/api/admin/plugins/marketplace/_id_/index.mjs +52 -46
- package/dist/astro/routes/api/admin/plugins/marketplace/_id_/index.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/marketplace/_id_/install.d.mts.map +1 -1
- package/dist/astro/routes/api/admin/plugins/marketplace/_id_/install.mjs +55 -46
- package/dist/astro/routes/api/admin/plugins/marketplace/_id_/install.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/marketplace/index.mjs +52 -46
- package/dist/astro/routes/api/admin/plugins/marketplace/index.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/registry/_id_/uninstall.d.mts.map +1 -1
- package/dist/astro/routes/api/admin/plugins/registry/_id_/uninstall.mjs +55 -46
- package/dist/astro/routes/api/admin/plugins/registry/_id_/uninstall.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/registry/_id_/update.d.mts.map +1 -1
- package/dist/astro/routes/api/admin/plugins/registry/_id_/update.mjs +56 -47
- package/dist/astro/routes/api/admin/plugins/registry/_id_/update.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/registry/artifact.mjs +52 -46
- package/dist/astro/routes/api/admin/plugins/registry/artifact.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/registry/install.d.mts.map +1 -1
- package/dist/astro/routes/api/admin/plugins/registry/install.mjs +56 -47
- package/dist/astro/routes/api/admin/plugins/registry/install.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/updates.mjs +52 -46
- package/dist/astro/routes/api/admin/plugins/updates.mjs.map +1 -1
- package/dist/astro/routes/api/admin/themes/marketplace/_id_/index.mjs +52 -46
- package/dist/astro/routes/api/admin/themes/marketplace/_id_/index.mjs.map +1 -1
- package/dist/astro/routes/api/admin/themes/marketplace/_id_/thumbnail.mjs +4 -4
- package/dist/astro/routes/api/admin/themes/marketplace/index.mjs +52 -46
- package/dist/astro/routes/api/admin/themes/marketplace/index.mjs.map +1 -1
- package/dist/astro/routes/api/admin/users/_id_/disable.mjs +4 -4
- package/dist/astro/routes/api/admin/users/_id_/enable.mjs +3 -3
- package/dist/astro/routes/api/admin/users/_id_/index.mjs +10 -9
- package/dist/astro/routes/api/admin/users/_id_/index.mjs.map +1 -1
- package/dist/astro/routes/api/admin/users/_id_/send-recovery.mjs +6 -6
- package/dist/astro/routes/api/admin/users/_id_/send-recovery.mjs.map +1 -1
- package/dist/astro/routes/api/admin/users/index.mjs +9 -8
- package/dist/astro/routes/api/admin/users/index.mjs.map +1 -1
- package/dist/astro/routes/api/auth/dev-bypass.mjs +8 -8
- package/dist/astro/routes/api/auth/invite/accept.mjs +3 -3
- package/dist/astro/routes/api/auth/invite/complete.mjs +14 -13
- package/dist/astro/routes/api/auth/invite/complete.mjs.map +1 -1
- package/dist/astro/routes/api/auth/invite/index.mjs +12 -11
- package/dist/astro/routes/api/auth/invite/index.mjs.map +1 -1
- package/dist/astro/routes/api/auth/invite/register-options.mjs +13 -12
- package/dist/astro/routes/api/auth/invite/register-options.mjs.map +1 -1
- package/dist/astro/routes/api/auth/logout.mjs +4 -4
- package/dist/astro/routes/api/auth/magic-link/send.mjs +14 -13
- package/dist/astro/routes/api/auth/magic-link/send.mjs.map +1 -1
- package/dist/astro/routes/api/auth/magic-link/verify.mjs +4 -4
- package/dist/astro/routes/api/auth/me.mjs +10 -9
- package/dist/astro/routes/api/auth/me.mjs.map +1 -1
- package/dist/astro/routes/api/auth/mode.mjs +4 -4
- package/dist/astro/routes/api/auth/oauth/_provider_/callback.mjs +4 -4
- package/dist/astro/routes/api/auth/oauth/_provider_.mjs +2 -2
- package/dist/astro/routes/api/auth/passkey/_id_.mjs +9 -8
- package/dist/astro/routes/api/auth/passkey/_id_.mjs.map +1 -1
- package/dist/astro/routes/api/auth/passkey/index.mjs +3 -3
- package/dist/astro/routes/api/auth/passkey/options.mjs +15 -14
- package/dist/astro/routes/api/auth/passkey/options.mjs.map +1 -1
- package/dist/astro/routes/api/auth/passkey/register/options.mjs +13 -12
- package/dist/astro/routes/api/auth/passkey/register/options.mjs.map +1 -1
- package/dist/astro/routes/api/auth/passkey/register/verify.mjs +14 -13
- package/dist/astro/routes/api/auth/passkey/register/verify.mjs.map +1 -1
- package/dist/astro/routes/api/auth/passkey/verify.mjs +14 -13
- package/dist/astro/routes/api/auth/passkey/verify.mjs.map +1 -1
- package/dist/astro/routes/api/auth/signup/complete.mjs +14 -13
- package/dist/astro/routes/api/auth/signup/complete.mjs.map +1 -1
- package/dist/astro/routes/api/auth/signup/request.mjs +14 -13
- package/dist/astro/routes/api/auth/signup/request.mjs.map +1 -1
- package/dist/astro/routes/api/auth/signup/verify.mjs +3 -3
- package/dist/astro/routes/api/comments/_collection_/_contentId_/index.mjs +20 -19
- package/dist/astro/routes/api/comments/_collection_/_contentId_/index.mjs.map +1 -1
- package/dist/astro/routes/api/comments/_collection_/_contentId_/reactions.mjs +17 -16
- package/dist/astro/routes/api/comments/_collection_/_contentId_/reactions.mjs.map +1 -1
- package/dist/astro/routes/api/content/_collection_/_id_/compare.mjs +4 -4
- package/dist/astro/routes/api/content/_collection_/_id_/discard-draft.mjs +5 -5
- package/dist/astro/routes/api/content/_collection_/_id_/duplicate.mjs +5 -5
- package/dist/astro/routes/api/content/_collection_/_id_/permanent.mjs +4 -4
- package/dist/astro/routes/api/content/_collection_/_id_/preview-url.mjs +15 -15
- package/dist/astro/routes/api/content/_collection_/_id_/publish.mjs +11 -10
- package/dist/astro/routes/api/content/_collection_/_id_/publish.mjs.map +1 -1
- package/dist/astro/routes/api/content/_collection_/_id_/references/_relation_/children.mjs +21 -20
- package/dist/astro/routes/api/content/_collection_/_id_/references/_relation_/children.mjs.map +1 -1
- package/dist/astro/routes/api/content/_collection_/_id_/references/_relation_/parents.mjs +21 -20
- package/dist/astro/routes/api/content/_collection_/_id_/references/_relation_/parents.mjs.map +1 -1
- package/dist/astro/routes/api/content/_collection_/_id_/restore.mjs +5 -5
- package/dist/astro/routes/api/content/_collection_/_id_/revisions.mjs +4 -4
- package/dist/astro/routes/api/content/_collection_/_id_/schedule.mjs +11 -10
- package/dist/astro/routes/api/content/_collection_/_id_/schedule.mjs.map +1 -1
- package/dist/astro/routes/api/content/_collection_/_id_/terms/_taxonomy_.d.mts.map +1 -1
- package/dist/astro/routes/api/content/_collection_/_id_/terms/_taxonomy_.mjs +47 -34
- package/dist/astro/routes/api/content/_collection_/_id_/terms/_taxonomy_.mjs.map +1 -1
- package/dist/astro/routes/api/content/_collection_/_id_/translations.mjs +4 -4
- package/dist/astro/routes/api/content/_collection_/_id_/unpublish.mjs +5 -5
- package/dist/astro/routes/api/content/_collection_/_id_.mjs +11 -10
- package/dist/astro/routes/api/content/_collection_/_id_.mjs.map +1 -1
- package/dist/astro/routes/api/content/_collection_/authors.mjs +4 -4
- package/dist/astro/routes/api/content/_collection_/index.mjs +11 -10
- package/dist/astro/routes/api/content/_collection_/index.mjs.map +1 -1
- package/dist/astro/routes/api/content/_collection_/trash.mjs +10 -9
- package/dist/astro/routes/api/content/_collection_/trash.mjs.map +1 -1
- package/dist/astro/routes/api/dashboard.mjs +12 -10
- package/dist/astro/routes/api/dashboard.mjs.map +1 -1
- package/dist/astro/routes/api/dev/emails.mjs +4 -4
- package/dist/astro/routes/api/import/probe.d.mts +3 -3
- package/dist/astro/routes/api/import/probe.mjs +22 -20
- package/dist/astro/routes/api/import/probe.mjs.map +1 -1
- package/dist/astro/routes/api/import/wordpress/analyze.mjs +5 -5
- package/dist/astro/routes/api/import/wordpress/execute.d.mts +11 -11
- package/dist/astro/routes/api/import/wordpress/execute.mjs +18 -18
- package/dist/astro/routes/api/import/wordpress/execute.mjs.map +1 -1
- package/dist/astro/routes/api/import/wordpress/media.d.mts +10 -10
- package/dist/astro/routes/api/import/wordpress/media.mjs +14 -12
- package/dist/astro/routes/api/import/wordpress/media.mjs.map +1 -1
- package/dist/astro/routes/api/import/wordpress/prepare.mjs +13 -12
- package/dist/astro/routes/api/import/wordpress/prepare.mjs.map +1 -1
- package/dist/astro/routes/api/import/wordpress/rewrite-urls.d.mts +11 -11
- package/dist/astro/routes/api/import/wordpress/rewrite-urls.mjs +15 -15
- package/dist/astro/routes/api/import/wordpress-plugin/analyze.d.mts +1 -1
- package/dist/astro/routes/api/import/wordpress-plugin/analyze.mjs +22 -20
- package/dist/astro/routes/api/import/wordpress-plugin/analyze.mjs.map +1 -1
- package/dist/astro/routes/api/import/wordpress-plugin/callback.mjs +1 -1
- package/dist/astro/routes/api/import/wordpress-plugin/execute.d.mts +11 -11
- package/dist/astro/routes/api/import/wordpress-plugin/execute.mjs +32 -31
- package/dist/astro/routes/api/import/wordpress-plugin/execute.mjs.map +1 -1
- package/dist/astro/routes/api/manifest.mjs +6 -6
- package/dist/astro/routes/api/mcp.d.mts.map +1 -1
- package/dist/astro/routes/api/mcp.mjs +174 -72
- package/dist/astro/routes/api/mcp.mjs.map +1 -1
- package/dist/astro/routes/api/media/_id_/confirm.mjs +14 -12
- package/dist/astro/routes/api/media/_id_/confirm.mjs.map +1 -1
- package/dist/astro/routes/api/media/_id_/upload.mjs +7 -7
- package/dist/astro/routes/api/media/_id_/usage.mjs +17 -16
- package/dist/astro/routes/api/media/_id_/usage.mjs.map +1 -1
- package/dist/astro/routes/api/media/_id_.mjs +16 -15
- package/dist/astro/routes/api/media/_id_.mjs.map +1 -1
- package/dist/astro/routes/api/media/file/_...key_.mjs +3 -3
- package/dist/astro/routes/api/media/providers/_providerId_/_itemId_.mjs +4 -4
- package/dist/astro/routes/api/media/providers/_providerId_/index.mjs +4 -4
- package/dist/astro/routes/api/media/providers/index.mjs +4 -4
- package/dist/astro/routes/api/media/upload-url.mjs +11 -10
- package/dist/astro/routes/api/media/upload-url.mjs.map +1 -1
- package/dist/astro/routes/api/media.mjs +19 -16
- package/dist/astro/routes/api/media.mjs.map +1 -1
- package/dist/astro/routes/api/menus/_name_/items/_id_.mjs +13 -13
- package/dist/astro/routes/api/menus/_name_/items.mjs +13 -13
- package/dist/astro/routes/api/menus/_name_/reorder.mjs +13 -13
- package/dist/astro/routes/api/menus/_name_/translations.mjs +13 -13
- package/dist/astro/routes/api/menus/_name_.mjs +13 -13
- package/dist/astro/routes/api/menus/index.mjs +13 -13
- package/dist/astro/routes/api/oauth/authorize.mjs +6 -6
- package/dist/astro/routes/api/oauth/device/authorize.mjs +7 -7
- package/dist/astro/routes/api/oauth/device/code.mjs +10 -10
- package/dist/astro/routes/api/oauth/device/token.mjs +9 -9
- package/dist/astro/routes/api/oauth/register.mjs +4 -4
- package/dist/astro/routes/api/oauth/token/refresh.mjs +7 -7
- package/dist/astro/routes/api/oauth/token/revoke.mjs +7 -7
- package/dist/astro/routes/api/oauth/token.mjs +7 -7
- package/dist/astro/routes/api/openapi.json.mjs +183 -6
- package/dist/astro/routes/api/openapi.json.mjs.map +1 -1
- package/dist/astro/routes/api/plugins/_pluginId_/_...path_.d.mts.map +1 -1
- package/dist/astro/routes/api/plugins/_pluginId_/_...path_.mjs +8 -7
- package/dist/astro/routes/api/plugins/_pluginId_/_...path_.mjs.map +1 -1
- package/dist/astro/routes/api/redirects/404s/index.mjs +14 -13
- package/dist/astro/routes/api/redirects/404s/index.mjs.map +1 -1
- package/dist/astro/routes/api/redirects/404s/summary.mjs +14 -13
- package/dist/astro/routes/api/redirects/404s/summary.mjs.map +1 -1
- package/dist/astro/routes/api/redirects/_id_.mjs +16 -14
- package/dist/astro/routes/api/redirects/_id_.mjs.map +1 -1
- package/dist/astro/routes/api/redirects/index.mjs +16 -14
- package/dist/astro/routes/api/redirects/index.mjs.map +1 -1
- package/dist/astro/routes/api/relations/_id_/index.mjs +21 -20
- package/dist/astro/routes/api/relations/_id_/index.mjs.map +1 -1
- package/dist/astro/routes/api/relations/_id_/translations.mjs +16 -15
- package/dist/astro/routes/api/relations/_id_/translations.mjs.map +1 -1
- package/dist/astro/routes/api/relations/index.mjs +21 -20
- package/dist/astro/routes/api/relations/index.mjs.map +1 -1
- package/dist/astro/routes/api/revisions/_revisionId_/index.mjs +4 -4
- package/dist/astro/routes/api/revisions/_revisionId_/restore.mjs +5 -5
- package/dist/astro/routes/api/schema/collections/_slug_/fields/_fieldSlug_.mjs +52 -46
- package/dist/astro/routes/api/schema/collections/_slug_/fields/_fieldSlug_.mjs.map +1 -1
- package/dist/astro/routes/api/schema/collections/_slug_/fields/index.mjs +52 -46
- package/dist/astro/routes/api/schema/collections/_slug_/fields/index.mjs.map +1 -1
- package/dist/astro/routes/api/schema/collections/_slug_/fields/reorder.mjs +52 -46
- package/dist/astro/routes/api/schema/collections/_slug_/fields/reorder.mjs.map +1 -1
- package/dist/astro/routes/api/schema/collections/_slug_/index.d.mts.map +1 -1
- package/dist/astro/routes/api/schema/collections/_slug_/index.mjs +52 -46
- package/dist/astro/routes/api/schema/collections/_slug_/index.mjs.map +1 -1
- package/dist/astro/routes/api/schema/collections/index.mjs +52 -46
- package/dist/astro/routes/api/schema/collections/index.mjs.map +1 -1
- package/dist/astro/routes/api/schema/collections/reorder.d.mts +8 -0
- package/dist/astro/routes/api/schema/collections/reorder.d.mts.map +1 -0
- package/dist/astro/routes/api/schema/collections/reorder.mjs +71 -0
- package/dist/astro/routes/api/schema/collections/reorder.mjs.map +1 -0
- package/dist/astro/routes/api/schema/index.mjs +14 -11
- package/dist/astro/routes/api/schema/index.mjs.map +1 -1
- package/dist/astro/routes/api/schema/orphans/_slug_.mjs +52 -46
- package/dist/astro/routes/api/schema/orphans/_slug_.mjs.map +1 -1
- package/dist/astro/routes/api/schema/orphans/index.mjs +52 -46
- package/dist/astro/routes/api/schema/orphans/index.mjs.map +1 -1
- package/dist/astro/routes/api/search/enable.d.mts +1 -0
- package/dist/astro/routes/api/search/enable.d.mts.map +1 -1
- package/dist/astro/routes/api/search/enable.mjs +19 -15
- package/dist/astro/routes/api/search/enable.mjs.map +1 -1
- package/dist/astro/routes/api/search/index.mjs +13 -13
- package/dist/astro/routes/api/search/rebuild.mjs +15 -15
- package/dist/astro/routes/api/search/rebuild.mjs.map +1 -1
- package/dist/astro/routes/api/search/stats.mjs +9 -9
- package/dist/astro/routes/api/search/suggest.mjs +13 -13
- package/dist/astro/routes/api/sections/_slug_.mjs +13 -12
- package/dist/astro/routes/api/sections/_slug_.mjs.map +1 -1
- package/dist/astro/routes/api/sections/index.mjs +13 -12
- package/dist/astro/routes/api/sections/index.mjs.map +1 -1
- package/dist/astro/routes/api/settings/backups/archives/_name_.mjs +8 -8
- package/dist/astro/routes/api/settings/backups/archives/index.mjs +7 -7
- package/dist/astro/routes/api/settings/backups/export.mjs +8 -8
- package/dist/astro/routes/api/settings/backups/index.mjs +9 -9
- package/dist/astro/routes/api/settings/email.mjs +6 -6
- package/dist/astro/routes/api/settings.mjs +18 -17
- package/dist/astro/routes/api/settings.mjs.map +1 -1
- package/dist/astro/routes/api/setup/admin-verify.mjs +15 -14
- package/dist/astro/routes/api/setup/admin-verify.mjs.map +1 -1
- package/dist/astro/routes/api/setup/admin.mjs +14 -13
- package/dist/astro/routes/api/setup/admin.mjs.map +1 -1
- package/dist/astro/routes/api/setup/dev-bypass.mjs +34 -32
- package/dist/astro/routes/api/setup/dev-bypass.mjs.map +1 -1
- package/dist/astro/routes/api/setup/dev-reset.mjs +4 -4
- package/dist/astro/routes/api/setup/index.mjs +36 -34
- package/dist/astro/routes/api/setup/index.mjs.map +1 -1
- package/dist/astro/routes/api/setup/status.mjs +5 -5
- package/dist/astro/routes/api/snapshot.mjs +10 -10
- package/dist/astro/routes/api/taxonomies/_name_/reorder.d.mts +8 -0
- package/dist/astro/routes/api/taxonomies/_name_/reorder.d.mts.map +1 -0
- package/dist/astro/routes/api/taxonomies/_name_/reorder.mjs +44 -0
- package/dist/astro/routes/api/taxonomies/_name_/reorder.mjs.map +1 -0
- package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_/translations.mjs +19 -18
- package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_/translations.mjs.map +1 -1
- package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_.mjs +19 -18
- package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_.mjs.map +1 -1
- package/dist/astro/routes/api/taxonomies/_name_/terms/index.d.mts.map +1 -1
- package/dist/astro/routes/api/taxonomies/_name_/terms/index.mjs +21 -19
- package/dist/astro/routes/api/taxonomies/_name_/terms/index.mjs.map +1 -1
- package/dist/astro/routes/api/taxonomies/_name_/translations.d.mts +8 -0
- package/dist/astro/routes/api/taxonomies/_name_/translations.d.mts.map +1 -0
- package/dist/astro/routes/api/taxonomies/_name_/translations.mjs +46 -0
- package/dist/astro/routes/api/taxonomies/_name_/translations.mjs.map +1 -0
- package/dist/astro/routes/api/taxonomies/_name_.d.mts +19 -0
- package/dist/astro/routes/api/taxonomies/_name_.d.mts.map +1 -0
- package/dist/astro/routes/api/taxonomies/_name_.mjs +88 -0
- package/dist/astro/routes/api/taxonomies/_name_.mjs.map +1 -0
- package/dist/astro/routes/api/taxonomies/index.mjs +19 -18
- package/dist/astro/routes/api/taxonomies/index.mjs.map +1 -1
- package/dist/astro/routes/api/themes/preview.mjs +8 -8
- package/dist/astro/routes/api/typegen.mjs +6 -6
- package/dist/astro/routes/api/well-known/auth.mjs +2 -2
- package/dist/astro/routes/api/well-known/oauth-authorization-server.mjs +2 -2
- package/dist/astro/routes/api/well-known/oauth-protected-resource.mjs +2 -2
- package/dist/astro/routes/api/widget-areas/_name_/reorder.mjs +10 -9
- package/dist/astro/routes/api/widget-areas/_name_/reorder.mjs.map +1 -1
- package/dist/astro/routes/api/widget-areas/_name_/widgets/_id_.mjs +13 -12
- package/dist/astro/routes/api/widget-areas/_name_/widgets/_id_.mjs.map +1 -1
- package/dist/astro/routes/api/widget-areas/_name_/widgets.mjs +13 -12
- package/dist/astro/routes/api/widget-areas/_name_/widgets.mjs.map +1 -1
- package/dist/astro/routes/api/widget-areas/_name_.mjs +8 -7
- package/dist/astro/routes/api/widget-areas/_name_.mjs.map +1 -1
- package/dist/astro/routes/api/widget-areas/index.mjs +13 -12
- package/dist/astro/routes/api/widget-areas/index.mjs.map +1 -1
- package/dist/astro/routes/api/widget-components.mjs +4 -4
- package/dist/astro/routes/robots.txt.mjs +11 -10
- package/dist/astro/routes/robots.txt.mjs.map +1 -1
- package/dist/astro/routes/sitemap-_collection_.xml.mjs +14 -14
- package/dist/astro/routes/sitemap.xml.mjs +12 -11
- package/dist/astro/routes/sitemap.xml.mjs.map +1 -1
- package/dist/astro/types.d.mts +41 -17
- package/dist/astro/types.d.mts.map +1 -1
- package/dist/auth/providers/github.d.mts +1 -1
- package/dist/auth/providers/google.d.mts +1 -1
- package/dist/{authorize-R5iYYrpa.mjs → authorize-1iFJzPaZ.mjs} +2 -2
- package/dist/{authorize-R5iYYrpa.mjs.map → authorize-1iFJzPaZ.mjs.map} +1 -1
- package/dist/{backup-LpQ1V_sR.mjs → backup-LHjIAVNP.mjs} +5 -5
- package/dist/{backup-LpQ1V_sR.mjs.map → backup-LHjIAVNP.mjs.map} +1 -1
- package/dist/{base64-B-PsqheR.mjs → base64-CvLhUKQv.mjs} +1 -1
- package/dist/{base64-B-PsqheR.mjs.map → base64-CvLhUKQv.mjs.map} +1 -1
- package/dist/bundle-CBEXrBnb.mjs +663 -0
- package/dist/bundle-CBEXrBnb.mjs.map +1 -0
- package/dist/{byline-fields-T9ZLZMxJ.mjs → byline-fields-C5S8wygO.mjs} +2 -2
- package/dist/{byline-fields-T9ZLZMxJ.mjs.map → byline-fields-C5S8wygO.mjs.map} +1 -1
- package/dist/{byline-C5TAqs8N.mjs → byline-pEcoDps8.mjs} +9 -9
- package/dist/{byline-C5TAqs8N.mjs.map → byline-pEcoDps8.mjs.map} +1 -1
- package/dist/{byline-registry-BCuOp4UF.mjs → byline-registry-DDun8sxh.mjs} +4 -4
- package/dist/{byline-registry-BCuOp4UF.mjs.map → byline-registry-DDun8sxh.mjs.map} +1 -1
- package/dist/{bylines-B8-WGdja.mjs → bylines-BWemdX2k.mjs} +8 -8
- package/dist/{bylines-B8-WGdja.mjs.map → bylines-BWemdX2k.mjs.map} +1 -1
- package/dist/{bylines-DGuqBLjV.mjs → bylines-DJfSfXCL.mjs} +5 -5
- package/dist/{bylines-DGuqBLjV.mjs.map → bylines-DJfSfXCL.mjs.map} +1 -1
- package/dist/cache-CBJaDy_E.mjs +108 -0
- package/dist/cache-CBJaDy_E.mjs.map +1 -0
- package/dist/{challenge-store-BFzgFRog.mjs → challenge-store-CZyFVaad.mjs} +1 -1
- package/dist/{challenge-store-BFzgFRog.mjs.map → challenge-store-CZyFVaad.mjs.map} +1 -1
- package/dist/{chunks-D5dlPeRb.mjs → chunks-DfPax1I0.mjs} +2 -2
- package/dist/{chunks-D5dlPeRb.mjs.map → chunks-DfPax1I0.mjs.map} +1 -1
- package/dist/cli/index.mjs +193 -707
- package/dist/cli/index.mjs.map +1 -1
- package/dist/client/cf-access.d.mts +1 -1
- package/dist/client/index.d.mts +74 -4
- package/dist/client/index.d.mts.map +1 -1
- package/dist/client/index.mjs +25 -2
- package/dist/client/index.mjs.map +1 -1
- package/dist/collection-deletion-C8BugSGz.mjs +779 -0
- package/dist/collection-deletion-C8BugSGz.mjs.map +1 -0
- package/dist/{comment-DnTxxVHv.mjs → comment-Dxlep6cE.mjs} +3 -3
- package/dist/{comment-DnTxxVHv.mjs.map → comment-Dxlep6cE.mjs.map} +1 -1
- package/dist/{comment-reaction-BUlUz2bh.mjs → comment-reaction-D6qfHKBc.mjs} +2 -2
- package/dist/{comment-reaction-BUlUz2bh.mjs.map → comment-reaction-D6qfHKBc.mjs.map} +1 -1
- package/dist/{comments-QPKgK3nk.mjs → comments-CTOuhTSX.mjs} +3 -3
- package/dist/{comments-QPKgK3nk.mjs.map → comments-CTOuhTSX.mjs.map} +1 -1
- package/dist/{components-DZkL4Fx0.mjs → components-CQy3lh9x.mjs} +1 -1
- package/dist/{components-DZkL4Fx0.mjs.map → components-CQy3lh9x.mjs.map} +1 -1
- package/dist/{config-DyF3OKg_.mjs → config-C3EBstHi.mjs} +18 -2
- package/dist/config-C3EBstHi.mjs.map +1 -0
- package/dist/{content-CpfKV9QE.mjs → content-BNfwh7xX.mjs} +667 -183
- package/dist/content-BNfwh7xX.mjs.map +1 -0
- package/dist/content-list-query-DhQ84eVV.d.mts +26 -0
- package/dist/content-list-query-DhQ84eVV.d.mts.map +1 -0
- package/dist/content-refresh-DNERP4-f.mjs +2560 -0
- package/dist/content-refresh-DNERP4-f.mjs.map +1 -0
- package/dist/{context-kO4YZB03.mjs → context-DRgOvSnW.mjs} +28 -20
- package/dist/context-DRgOvSnW.mjs.map +1 -0
- package/dist/{cron-BlKIMD_e.mjs → cron-Y9aLtgrA.mjs} +1 -1
- package/dist/{cron-BlKIMD_e.mjs.map → cron-Y9aLtgrA.mjs.map} +1 -1
- package/dist/{dashboard-DoBwPnON.mjs → dashboard-CB8J89rJ.mjs} +45 -11
- package/dist/dashboard-CB8J89rJ.mjs.map +1 -0
- package/dist/db/index.d.mts +4 -4
- package/dist/db/index.mjs +3 -3
- package/dist/db/index.mjs.map +1 -1
- package/dist/db/libsql.d.mts +1 -1
- package/dist/db/postgres.d.mts +1 -1
- package/dist/db/sqlite.d.mts +1 -1
- package/dist/{db-errors-CcWLaRiR.mjs → db-errors-Cl-sKYTm.mjs} +1 -1
- package/dist/{db-errors-CcWLaRiR.mjs.map → db-errors-Cl-sKYTm.mjs.map} +1 -1
- package/dist/{default-C4v9KP56.mjs → default-DdDl3fxv.mjs} +1 -1
- package/dist/{default-C4v9KP56.mjs.map → default-DdDl3fxv.mjs.map} +1 -1
- package/dist/deferred-tasks-BMsa9F64.d.mts +9 -0
- package/dist/deferred-tasks-BMsa9F64.d.mts.map +1 -0
- package/dist/{device-flow-DhUsyJU6.mjs → device-flow-CaioYGK3.mjs} +5 -5
- package/dist/{device-flow-DhUsyJU6.mjs.map → device-flow-CaioYGK3.mjs.map} +1 -1
- package/dist/{dialect-helpers-DFwyWjTM.mjs → dialect-helpers-BSbfaO3F.mjs} +24 -16
- package/dist/dialect-helpers-BSbfaO3F.mjs.map +1 -0
- package/dist/dist-D_6CKR8z.mjs +810 -0
- package/dist/dist-D_6CKR8z.mjs.map +1 -0
- package/dist/{email-console-C-9Ng8DM.mjs → email-console-BHNjIwiB.mjs} +1 -1
- package/dist/{email-console-C-9Ng8DM.mjs.map → email-console-BHNjIwiB.mjs.map} +1 -1
- package/dist/{enrich-CFJJgxs_.mjs → enrich-_ySMEvEE.mjs} +2 -2
- package/dist/{enrich-CFJJgxs_.mjs.map → enrich-_ySMEvEE.mjs.map} +1 -1
- package/dist/{error-DmmN74gW.mjs → error-DupyJZE2.mjs} +3 -3
- package/dist/{error-DmmN74gW.mjs.map → error-DupyJZE2.mjs.map} +1 -1
- package/dist/{errors-DtEXIQQV.mjs → errors-CLUPm7aD.mjs} +15 -2
- package/dist/errors-CLUPm7aD.mjs.map +1 -0
- package/dist/{escape-CdW31WsG.mjs → escape-C_aLKP3H.mjs} +1 -1
- package/dist/{escape-CdW31WsG.mjs.map → escape-C_aLKP3H.mjs.map} +1 -1
- package/dist/{field-defs-cache-QMVnzTH6.mjs → field-defs-cache-hg6QkjV9.mjs} +5 -5
- package/dist/{field-defs-cache-QMVnzTH6.mjs.map → field-defs-cache-hg6QkjV9.mjs.map} +1 -1
- package/dist/{fts-manager-vncsZIJm.mjs → fts-manager-CZ-5dVwJ.mjs} +111 -61
- package/dist/fts-manager-CZ-5dVwJ.mjs.map +1 -0
- package/dist/{hash-Cb7U1b5M.mjs → hash-Cg0TXJPV.mjs} +1 -1
- package/dist/{hash-Cb7U1b5M.mjs.map → hash-Cg0TXJPV.mjs.map} +1 -1
- package/dist/{hreflang-CZbSx6Bg.d.mts → hreflang-D3Uehfv2.d.mts} +2 -2
- package/dist/{hreflang-CZbSx6Bg.d.mts.map → hreflang-D3Uehfv2.d.mts.map} +1 -1
- package/dist/{hreflang-Bg-zMpkr.mjs → hreflang-DNswoYsi.mjs} +7 -7
- package/dist/{hreflang-Bg-zMpkr.mjs.map → hreflang-DNswoYsi.mjs.map} +1 -1
- package/dist/{import-PKhLeXvn.mjs → import-B41u8Rot.mjs} +7 -7
- package/dist/{import-PKhLeXvn.mjs.map → import-B41u8Rot.mjs.map} +1 -1
- package/dist/{index-j9rTh3cp.d.mts → index-C3Y-rsyN.d.mts} +286 -93
- package/dist/index-C3Y-rsyN.d.mts.map +1 -0
- package/dist/{index-BeNto_Vs.d.mts → index-Cof4-Fab.d.mts} +5 -10
- package/dist/index-Cof4-Fab.d.mts.map +1 -0
- package/dist/index.d.mts +20 -19
- package/dist/index.mjs +79 -71
- package/dist/{init-lock-DJkX6Hto.mjs → init-lock-6b309ZrF.mjs} +1 -1
- package/dist/{init-lock-DJkX6Hto.mjs.map → init-lock-6b309ZrF.mjs.map} +1 -1
- package/dist/{load-BwTdWE8B.mjs → load-DmvJrtxS.mjs} +2 -2
- package/dist/{load-BwTdWE8B.mjs.map → load-DmvJrtxS.mjs.map} +1 -1
- package/dist/{loader-C1XOLV5b.mjs → loader-DLBqRiGP.mjs} +120 -64
- package/dist/loader-DLBqRiGP.mjs.map +1 -0
- package/dist/{manifest-schema-bCq54i7F.mjs → manifest-schema-BXbUAHod.mjs} +1 -1
- package/dist/{manifest-schema-bCq54i7F.mjs.map → manifest-schema-BXbUAHod.mjs.map} +1 -1
- package/dist/media/image-endpoint.mjs +1 -1
- package/dist/media/index.d.mts +1 -1
- package/dist/media/index.mjs +3 -2
- package/dist/media/index.mjs.map +1 -1
- package/dist/media/local-runtime.d.mts +12 -12
- package/dist/media/local-runtime.mjs +10 -9
- package/dist/media/local-runtime.mjs.map +1 -1
- package/dist/{media-DWIzoVFL.mjs → media-BWWWb8kg.mjs} +92 -3
- package/dist/media-BWWWb8kg.mjs.map +1 -0
- package/dist/{media-tQDZEdu7.mjs → media-CzhAn2--.mjs} +2 -2
- package/dist/{media-tQDZEdu7.mjs.map → media-CzhAn2--.mjs.map} +1 -1
- package/dist/{media-allowlist-B7TPD6aq.mjs → media-allowlist-BD6HhgXA.mjs} +14 -3
- package/dist/media-allowlist-BD6HhgXA.mjs.map +1 -0
- package/dist/{media-upload-Bc9Nc5Zf.mjs → media-upload-9yas9zGH.mjs} +11 -9
- package/dist/{media-upload-Bc9Nc5Zf.mjs.map → media-upload-9yas9zGH.mjs.map} +1 -1
- package/dist/{media-url-BCm5vBn6.mjs → media-url-BvoX88FA.mjs} +1 -1
- package/dist/{media-url-BCm5vBn6.mjs.map → media-url-BvoX88FA.mjs.map} +1 -1
- package/dist/{media-usage-DPJcNaAq.mjs → media-usage-De3qvwjc.mjs} +138 -61
- package/dist/media-usage-De3qvwjc.mjs.map +1 -0
- package/dist/media-usage-work-B9Bksh_b.mjs +152 -0
- package/dist/media-usage-work-B9Bksh_b.mjs.map +1 -0
- package/dist/media-usage-work-DrCbqBcl.mjs +424 -0
- package/dist/media-usage-work-DrCbqBcl.mjs.map +1 -0
- package/dist/media-usage-write-fence-aHEgN1zB.mjs +42 -0
- package/dist/media-usage-write-fence-aHEgN1zB.mjs.map +1 -0
- package/dist/{menus-C6qhqP4X.mjs → menus-CC-yn1F9.mjs} +554 -121
- package/dist/menus-CC-yn1F9.mjs.map +1 -0
- package/dist/{menus-BxPLFfIc.mjs → menus-fxvEDYE6.mjs} +5 -5
- package/dist/{menus-BxPLFfIc.mjs.map → menus-fxvEDYE6.mjs.map} +1 -1
- package/dist/{mode-fiXRMfeA.mjs → mode-Bpe-tNNy.mjs} +1 -1
- package/dist/{mode-fiXRMfeA.mjs.map → mode-Bpe-tNNy.mjs.map} +1 -1
- package/dist/{normalize-C-SHXmra.mjs → normalize-Bc-s_r-a.mjs} +1 -1
- package/dist/{normalize-C-SHXmra.mjs.map → normalize-Bc-s_r-a.mjs.map} +1 -1
- package/dist/{oauth-authorization-C9tFinly.mjs → oauth-authorization-BXO1MKej.mjs} +5 -5
- package/dist/{oauth-authorization-C9tFinly.mjs.map → oauth-authorization-BXO1MKej.mjs.map} +1 -1
- package/dist/{oauth-clients-BwTxE1aE.mjs → oauth-clients-C_DNqtRu.mjs} +1 -1
- package/dist/{oauth-clients-BwTxE1aE.mjs.map → oauth-clients-C_DNqtRu.mjs.map} +1 -1
- package/dist/{oauth-state-store-DaRUwPWO.mjs → oauth-state-store-DwAxMtk4.mjs} +1 -1
- package/dist/{oauth-state-store-DaRUwPWO.mjs.map → oauth-state-store-DwAxMtk4.mjs.map} +1 -1
- package/dist/{oauth-user-lookup-CzuzOSlF.mjs → oauth-user-lookup-BxfZizdk.mjs} +1 -1
- package/dist/{oauth-user-lookup-CzuzOSlF.mjs.map → oauth-user-lookup-BxfZizdk.mjs.map} +1 -1
- package/dist/object-cache/memory.d.mts +1 -1
- package/dist/{object-cache-Bok5j2ae.mjs → object-cache-71tPfV4A.mjs} +106 -5
- package/dist/object-cache-71tPfV4A.mjs.map +1 -0
- package/dist/{options-BlmBHTvX.mjs → options-CBXIHUoT.mjs} +1 -1
- package/dist/{options-BlmBHTvX.mjs.map → options-CBXIHUoT.mjs.map} +1 -1
- package/dist/{options-aeIs8WFu.d.mts → options-DRe1pIBL.d.mts} +4 -3
- package/dist/options-DRe1pIBL.d.mts.map +1 -0
- package/dist/page/index.d.mts +2 -2
- package/dist/{parse-BL49yb9D.mjs → parse-DFbuc-Fh.mjs} +2 -2
- package/dist/{parse-BL49yb9D.mjs.map → parse-DFbuc-Fh.mjs.map} +1 -1
- package/dist/{passkey-config-KMhgFjdQ.mjs → passkey-config-_2GOwwK8.mjs} +1 -1
- package/dist/{passkey-config-KMhgFjdQ.mjs.map → passkey-config-_2GOwwK8.mjs.map} +1 -1
- package/dist/{placeholder-Dxo5Zt58.d.mts → placeholder-CzW7Lshs.d.mts} +1 -1
- package/dist/{placeholder-Dxo5Zt58.d.mts.map → placeholder-CzW7Lshs.d.mts.map} +1 -1
- package/dist/plugin-types.d.mts +8 -1
- package/dist/plugin-types.d.mts.map +1 -1
- package/dist/plugin-utils.d.mts +10 -10
- package/dist/plugins/adapt-sandbox-entry.d.mts +10 -10
- package/dist/plugins/adapt-sandbox-entry.mjs +7 -4
- package/dist/plugins/adapt-sandbox-entry.mjs.map +1 -1
- package/dist/{portable-text-CNahFfj6.mjs → portable-text-DhKbDAGr.mjs} +1 -1
- package/dist/{portable-text-CNahFfj6.mjs.map → portable-text-DhKbDAGr.mjs.map} +1 -1
- package/dist/{preview-D4Jnbfx7.mjs → preview-PW-nOnq4.mjs} +2 -2
- package/dist/{preview-D4Jnbfx7.mjs.map → preview-PW-nOnq4.mjs.map} +1 -1
- package/dist/{public-url-DSGTnJFw.mjs → public-url-CWoFhQT8.mjs} +14 -3
- package/dist/public-url-CWoFhQT8.mjs.map +1 -0
- package/dist/{query-DCiXI7OZ.mjs → query-BgjfiLUQ.mjs} +38 -77
- package/dist/query-BgjfiLUQ.mjs.map +1 -0
- package/dist/{query-1xOcEy18.mjs → query-vgZNwIhh.mjs} +4 -4
- package/dist/{query-1xOcEy18.mjs.map → query-vgZNwIhh.mjs.map} +1 -1
- package/dist/{rate-limit-Vkp9gQ1Y.mjs → rate-limit-L5anykgo.mjs} +2 -2
- package/dist/{rate-limit-Vkp9gQ1Y.mjs.map → rate-limit-L5anykgo.mjs.map} +1 -1
- package/dist/{redirect-BJ6d1yHh.mjs → redirect-COsS6SWJ.mjs} +3 -3
- package/dist/{redirect-BJ6d1yHh.mjs.map → redirect-COsS6SWJ.mjs.map} +1 -1
- package/dist/{redirect-CDgfv9vh.mjs → redirect-KBNE3UEs.mjs} +1 -1
- package/dist/{redirect-CDgfv9vh.mjs.map → redirect-KBNE3UEs.mjs.map} +1 -1
- package/dist/{redirects-Cwkq2H-G.mjs → redirects-CufRQt0Q.mjs} +5 -5
- package/dist/{redirects-Cwkq2H-G.mjs.map → redirects-CufRQt0Q.mjs.map} +1 -1
- package/dist/{redirects-BOn2DIUs.mjs → redirects-cV9g7V-0.mjs} +149 -16
- package/dist/redirects-cV9g7V-0.mjs.map +1 -0
- package/dist/registry-C581V4TP.mjs +1275 -0
- package/dist/registry-C581V4TP.mjs.map +1 -0
- package/dist/{relation-DJ5DZh7B.mjs → relation-DtmAQH7s.mjs} +3 -3
- package/dist/{relation-DJ5DZh7B.mjs.map → relation-DtmAQH7s.mjs.map} +1 -1
- package/dist/{relations-C4duJnwI.mjs → relations-BBApI4e2.mjs} +3 -3
- package/dist/{relations-C4duJnwI.mjs.map → relations-BBApI4e2.mjs.map} +1 -1
- package/dist/{relations-DpTjftCl.mjs → relations-DICSyfsh.mjs} +6 -6
- package/dist/{relations-DpTjftCl.mjs.map → relations-DICSyfsh.mjs.map} +1 -1
- package/dist/{relations-DHFQGX0h.d.mts → relations-DItI3MN9.d.mts} +401 -40
- package/dist/relations-DItI3MN9.d.mts.map +1 -0
- package/dist/request-context.d.mts +3 -0
- package/dist/request-context.d.mts.map +1 -1
- package/dist/request-context.mjs.map +1 -1
- package/dist/{request-meta-DzXYYI-n.mjs → request-meta-DgZJh3zG.mjs} +2 -2
- package/dist/{request-meta-DzXYYI-n.mjs.map → request-meta-DgZJh3zG.mjs.map} +1 -1
- package/dist/{resolve-BUvFE0Lr.mjs → resolve-DOhVEvty.mjs} +2 -2
- package/dist/{resolve-BUvFE0Lr.mjs.map → resolve-DOhVEvty.mjs.map} +1 -1
- package/dist/{runner-BsI18UgP.mjs → runner-BiDvq5dv.mjs} +1404 -235
- package/dist/runner-BiDvq5dv.mjs.map +1 -0
- package/dist/{runner-CHdkERbt.d.mts → runner-CgQbELYC.d.mts} +2 -2
- package/dist/{runner-CHdkERbt.d.mts.map → runner-CgQbELYC.d.mts.map} +1 -1
- package/dist/runtime.d.mts +11 -11
- package/dist/runtime.mjs +5 -4
- package/dist/runtime.mjs.map +1 -1
- package/dist/{schema-eqZhkR0S.mjs → schema-BFyTg38T.mjs} +3 -3
- package/dist/{schema-eqZhkR0S.mjs.map → schema-BFyTg38T.mjs.map} +1 -1
- package/dist/schema-CeQSNwB3.mjs +390 -0
- package/dist/schema-CeQSNwB3.mjs.map +1 -0
- package/dist/{scopes-Be2d8bSb.mjs → scopes-BVeuhGBE.mjs} +3 -3
- package/dist/{scopes-Be2d8bSb.mjs.map → scopes-BVeuhGBE.mjs.map} +1 -1
- package/dist/{search-CeO678cp.mjs → search-BpuPKZb0.mjs} +26 -16
- package/dist/search-BpuPKZb0.mjs.map +1 -0
- package/dist/{secrets-870d-7yA.mjs → secrets-dtYs2hDo.mjs} +5 -5
- package/dist/{secrets-870d-7yA.mjs.map → secrets-dtYs2hDo.mjs.map} +1 -1
- package/dist/{sections-DSuEfirn.mjs → sections-COctk6hf.mjs} +3 -3
- package/dist/{sections-DSuEfirn.mjs.map → sections-COctk6hf.mjs.map} +1 -1
- package/dist/seed/index.d.mts +2 -2
- package/dist/seed/index.mjs +28 -26
- package/dist/seo/index.d.mts +3 -3
- package/dist/seo/index.mjs +20 -19
- package/dist/seo/index.mjs.map +1 -1
- package/dist/{seo-C3GDfT0V.mjs → seo-AFpVvTqr.mjs} +6 -4
- package/dist/seo-AFpVvTqr.mjs.map +1 -0
- package/dist/{seo-BAPKiED8.mjs → seo-CvDxfvFb.mjs} +3 -3
- package/dist/{seo-BAPKiED8.mjs.map → seo-CvDxfvFb.mjs.map} +1 -1
- package/dist/{service-DyFT-k-R.mjs → service-xMiMSTsR.mjs} +4 -4
- package/dist/{service-DyFT-k-R.mjs.map → service-xMiMSTsR.mjs.map} +1 -1
- package/dist/{session-user-BrK2zz7S.mjs → session-user-BzVemLkw.mjs} +2 -2
- package/dist/{session-user-BrK2zz7S.mjs.map → session-user-BzVemLkw.mjs.map} +1 -1
- package/dist/{settings-DgKouY2S.mjs → settings-B372MV0p.mjs} +7 -7
- package/dist/{settings-DgKouY2S.mjs.map → settings-B372MV0p.mjs.map} +1 -1
- package/dist/{settings-CREKyj8I.mjs → settings-BE9aqn_9.mjs} +3 -3
- package/dist/{settings-CREKyj8I.mjs.map → settings-BE9aqn_9.mjs.map} +1 -1
- package/dist/{setup-complete-L7bMK2VI.mjs → setup-complete-DF7TAfD5.mjs} +2 -2
- package/dist/{setup-complete-L7bMK2VI.mjs.map → setup-complete-DF7TAfD5.mjs.map} +1 -1
- package/dist/{setup-nonce-Ct5tjWcq.mjs → setup-nonce-Do-U_cg9.mjs} +1 -1
- package/dist/{setup-nonce-Ct5tjWcq.mjs.map → setup-nonce-Do-U_cg9.mjs.map} +1 -1
- package/dist/{single-flight-cache-C2exrGAi.mjs → single-flight-cache-B0eLKS6K.mjs} +2 -2
- package/dist/{single-flight-cache-C2exrGAi.mjs.map → single-flight-cache-B0eLKS6K.mjs.map} +1 -1
- package/dist/site-url-Bowsi9T-.mjs +16 -0
- package/dist/site-url-Bowsi9T-.mjs.map +1 -0
- package/dist/slugify-B994zkXC.mjs +17 -0
- package/dist/slugify-B994zkXC.mjs.map +1 -0
- package/dist/{snapshot-BdpUJKD-.mjs → snapshot-B5h6aPMh.mjs} +2 -2
- package/dist/{snapshot-BdpUJKD-.mjs.map → snapshot-B5h6aPMh.mjs.map} +1 -1
- package/dist/{ssrf-CviKqWmq.mjs → ssrf-Cpvg_n3A.mjs} +1 -1
- package/dist/{ssrf-CviKqWmq.mjs.map → ssrf-Cpvg_n3A.mjs.map} +1 -1
- package/dist/{state-xxv6ZTMv.mjs → state-DYwPue40.mjs} +1 -1
- package/dist/{state-xxv6ZTMv.mjs.map → state-DYwPue40.mjs.map} +1 -1
- package/dist/{status-COfycGIU.mjs → status-CWVYpbD8.mjs} +1 -1
- package/dist/{status-COfycGIU.mjs.map → status-CWVYpbD8.mjs.map} +1 -1
- package/dist/storage/local.d.mts +1 -1
- package/dist/storage/local.mjs +1 -1
- package/dist/storage/s3.d.mts +1 -1
- package/dist/storage/s3.mjs +1 -1
- package/dist/{taxonomies-B61CRSha.mjs → taxonomies-B0o9ExNV.mjs} +85 -75
- package/dist/taxonomies-B0o9ExNV.mjs.map +1 -0
- package/dist/{taxonomies-Dbdki3Un.mjs → taxonomies-Dk3MAklL.mjs} +360 -56
- package/dist/taxonomies-Dk3MAklL.mjs.map +1 -0
- package/dist/taxonomy-CPeM8d2o.mjs +484 -0
- package/dist/taxonomy-CPeM8d2o.mjs.map +1 -0
- package/dist/{tokens-DVltpO2D.mjs → tokens-C39klX8R.mjs} +2 -2
- package/dist/{tokens-DVltpO2D.mjs.map → tokens-C39klX8R.mjs.map} +1 -1
- package/dist/{transaction-D0FOsb3X.mjs → transaction-NQj4VJ7Z.mjs} +1 -1
- package/dist/{transaction-D0FOsb3X.mjs.map → transaction-NQj4VJ7Z.mjs.map} +1 -1
- package/dist/{transport-BWQo0Foy.mjs → transport--CfOflrp.mjs} +1 -1
- package/dist/{transport-BWQo0Foy.mjs.map → transport--CfOflrp.mjs.map} +1 -1
- package/dist/{transport-DokJOScS.d.mts → transport-B_rfeU-a.d.mts} +1 -1
- package/dist/{transport-DokJOScS.d.mts.map → transport-B_rfeU-a.d.mts.map} +1 -1
- package/dist/{trusted-proxy-CwjQj0YG.mjs → trusted-proxy-CEVekpby.mjs} +1 -1
- package/dist/{trusted-proxy-CwjQj0YG.mjs.map → trusted-proxy-CEVekpby.mjs.map} +1 -1
- package/dist/{types-o7xo7VgH.mjs → types-75KIqzL9.mjs} +20 -3
- package/dist/types-75KIqzL9.mjs.map +1 -0
- package/dist/{types-C9P3ysxq.d.mts → types-BPuloofb.d.mts} +1 -1
- package/dist/{types-C9P3ysxq.d.mts.map → types-BPuloofb.d.mts.map} +1 -1
- package/dist/{types-BD40g7zz.mjs → types-BW1syrlw.mjs} +1 -1
- package/dist/{types-BD40g7zz.mjs.map → types-BW1syrlw.mjs.map} +1 -1
- package/dist/{types-jISEalTX.d.mts → types-C3_AbkDP.d.mts} +53 -10
- package/dist/types-C3_AbkDP.d.mts.map +1 -0
- package/dist/{types-84Bque3-.d.mts → types-CAGdePf3.d.mts} +133 -1
- package/dist/types-CAGdePf3.d.mts.map +1 -0
- package/dist/{types-CcSZCcF1.d.mts → types-CFUfyt1s.d.mts} +1 -1
- package/dist/{types-CcSZCcF1.d.mts.map → types-CFUfyt1s.d.mts.map} +1 -1
- package/dist/{types-DQLZ58GV.d.mts → types-CPx2lIGo.d.mts} +1 -1
- package/dist/{types-DQLZ58GV.d.mts.map → types-CPx2lIGo.d.mts.map} +1 -1
- package/dist/{types-XrQQ-Aex.mjs → types-CQPWhPxE.mjs} +15 -11
- package/dist/types-CQPWhPxE.mjs.map +1 -0
- package/dist/{types-BQY7VYw6.d.mts → types-Cs-TFLNf.d.mts} +28 -8
- package/dist/types-Cs-TFLNf.d.mts.map +1 -0
- package/dist/types-CtzytVXn.mjs +18 -0
- package/dist/types-CtzytVXn.mjs.map +1 -0
- package/dist/{types-DqZVPILm.d.mts → types-D4_1Ga4L.d.mts} +44 -5
- package/dist/types-D4_1Ga4L.d.mts.map +1 -0
- package/dist/{types-Dr2GLlwu.d.mts → types-DefV3OiO.d.mts} +1 -1
- package/dist/{types-Dr2GLlwu.d.mts.map → types-DefV3OiO.d.mts.map} +1 -1
- package/dist/{types-DsAMyD_o.d.mts → types-X0MThr7x.d.mts} +1 -1
- package/dist/{types-DsAMyD_o.d.mts.map → types-X0MThr7x.d.mts.map} +1 -1
- package/dist/{upload-attempts-C5yd6Gae.mjs → upload-attempts-hlne3iA1.mjs} +1 -1
- package/dist/{upload-attempts-C5yd6Gae.mjs.map → upload-attempts-hlne3iA1.mjs.map} +1 -1
- package/dist/url-pattern-BHS0V5Ng.mjs +33 -0
- package/dist/url-pattern-BHS0V5Ng.mjs.map +1 -0
- package/dist/{user-BAumEmpA.mjs → user-sHgJ62SP.mjs} +3 -3
- package/dist/{user-BAumEmpA.mjs.map → user-sHgJ62SP.mjs.map} +1 -1
- package/dist/{utils-BbUgmeZG.mjs → utils-D4vD_Y98.mjs} +3 -3
- package/dist/{utils-BbUgmeZG.mjs.map → utils-D4vD_Y98.mjs.map} +1 -1
- package/dist/{validate-Bs_wT2ul.mjs → validate-BkqIG_Uj.mjs} +24 -5
- package/dist/validate-BkqIG_Uj.mjs.map +1 -0
- package/dist/{validate-VPnKoIzW.mjs → validate-BpwoDOgt.mjs} +25 -2
- package/dist/validate-BpwoDOgt.mjs.map +1 -0
- package/dist/{validate-CdYcY-Ln.d.mts → validate-DeGtBN84.d.mts} +25 -7
- package/dist/validate-DeGtBN84.d.mts.map +1 -0
- package/dist/{validation-BngR0nit.mjs → validation-CiJb_X5o.mjs} +14 -13
- package/dist/{validation-BngR0nit.mjs.map → validation-CiJb_X5o.mjs.map} +1 -1
- package/dist/version-x0NjTScG.mjs +7 -0
- package/dist/{version-Bdfb34kk.mjs.map → version-x0NjTScG.mjs.map} +1 -1
- package/dist/{widgets-L-VndTKD.mjs → widgets-Cwzvww0W.mjs} +3 -3
- package/dist/{widgets-L-VndTKD.mjs.map → widgets-Cwzvww0W.mjs.map} +1 -1
- package/dist/{wxr-taxonomies-DueaLFd9.d.mts → wxr-taxonomies-CmC0ZsDr.d.mts} +1 -1
- package/dist/{wxr-taxonomies-DueaLFd9.d.mts.map → wxr-taxonomies-CmC0ZsDr.d.mts.map} +1 -1
- package/dist/{wxr-taxonomies-BexjtNid.mjs → wxr-taxonomies-QZjWxjyk.mjs} +13 -32
- package/dist/{wxr-taxonomies-BexjtNid.mjs.map → wxr-taxonomies-QZjWxjyk.mjs.map} +1 -1
- package/dist/{zod-generator-DMzfga3g.mjs → zod-generator-D11MhxG4.mjs} +53 -6
- package/dist/zod-generator-D11MhxG4.mjs.map +1 -0
- package/package.json +18 -7
- package/src/after.ts +6 -1
- package/src/api/errors.ts +13 -0
- package/src/api/handlers/content.ts +172 -33
- package/src/api/handlers/dashboard.ts +12 -2
- package/src/api/handlers/index.ts +16 -0
- package/src/api/handlers/manifest.ts +2 -0
- package/src/api/handlers/marketplace.ts +14 -0
- package/src/api/handlers/media-allowlist.ts +13 -2
- package/src/api/handlers/media-usage-work.ts +188 -0
- package/src/api/handlers/media-usage.ts +5 -3
- package/src/api/handlers/registry.ts +14 -0
- package/src/api/handlers/revision.ts +19 -5
- package/src/api/handlers/schema.ts +46 -3
- package/src/api/handlers/seo.ts +5 -2
- package/src/api/handlers/snapshot.ts +1 -1
- package/src/api/handlers/taxonomies.ts +446 -83
- package/src/api/media-usage-write-fence.ts +64 -0
- package/src/api/openapi/document.ts +247 -0
- package/src/api/public-url.ts +14 -2
- package/src/api/schemas/common.ts +3 -1
- package/src/api/schemas/content.ts +122 -1
- package/src/api/schemas/media-usage.ts +133 -0
- package/src/api/schemas/schema.ts +91 -3
- package/src/api/schemas/search.ts +2 -0
- package/src/api/schemas/taxonomies.ts +58 -8
- package/src/api/site-url.ts +16 -6
- package/src/api/types.ts +1 -0
- package/src/astro/integration/index.ts +55 -42
- package/src/astro/integration/routes.ts +48 -4
- package/src/astro/integration/runtime.ts +28 -0
- package/src/astro/integration/virtual-modules.ts +27 -1
- package/src/astro/integration/vite-config.ts +16 -1
- package/src/astro/middleware/media-usage-write-fence.ts +30 -0
- package/src/astro/middleware/redirect.ts +14 -12
- package/src/astro/middleware/scoped-db.ts +35 -4
- package/src/astro/middleware.ts +131 -30
- package/src/astro/routes/api/admin/comments/[id]/status.ts +1 -1
- package/src/astro/routes/api/admin/media-usage/collection-deletions/index.ts +22 -0
- package/src/astro/routes/api/admin/media-usage/collection-deletions/retry.ts +22 -0
- package/src/astro/routes/api/admin/media-usage/work/index.ts +26 -0
- package/src/astro/routes/api/admin/media-usage/work/retry.ts +26 -0
- package/src/astro/routes/api/admin/plugins/[id]/disable.ts +4 -0
- package/src/astro/routes/api/admin/plugins/[id]/enable.ts +4 -0
- package/src/astro/routes/api/admin/plugins/[id]/uninstall.ts +4 -0
- package/src/astro/routes/api/admin/plugins/[id]/update.ts +4 -0
- package/src/astro/routes/api/admin/plugins/marketplace/[id]/install.ts +4 -0
- package/src/astro/routes/api/admin/plugins/registry/[id]/uninstall.ts +4 -0
- package/src/astro/routes/api/admin/plugins/registry/[id]/update.ts +4 -0
- package/src/astro/routes/api/admin/plugins/registry/install.ts +4 -0
- package/src/astro/routes/api/admin/users/[id]/send-recovery.ts +2 -2
- package/src/astro/routes/api/auth/invite/index.ts +2 -2
- package/src/astro/routes/api/auth/magic-link/send.ts +2 -2
- package/src/astro/routes/api/auth/signup/request.ts +2 -2
- package/src/astro/routes/api/comments/[collection]/[contentId]/index.ts +1 -1
- package/src/astro/routes/api/content/[collection]/[id]/terms/[taxonomy].ts +67 -31
- package/src/astro/routes/api/import/wordpress/execute.ts +3 -10
- package/src/astro/routes/api/mcp.ts +1 -0
- package/src/astro/routes/api/plugins/[pluginId]/[...path].ts +7 -1
- package/src/astro/routes/api/schema/collections/[slug]/index.ts +1 -7
- package/src/astro/routes/api/schema/collections/reorder.ts +31 -0
- package/src/astro/routes/api/schema/index.ts +2 -0
- package/src/astro/routes/api/search/enable.ts +5 -1
- package/src/astro/routes/api/search/rebuild.ts +6 -1
- package/src/astro/routes/api/taxonomies/[name]/reorder.ts +41 -0
- package/src/astro/routes/api/taxonomies/[name]/terms/index.ts +1 -0
- package/src/astro/routes/api/taxonomies/[name]/translations.ts +48 -0
- package/src/astro/routes/api/taxonomies/[name].ts +102 -0
- package/src/astro/types.ts +31 -4
- package/src/cleanup.ts +25 -26
- package/src/cli/commands/dev.ts +13 -3
- package/src/cli/commands/doctor.ts +193 -6
- package/src/cli/commands/export-seed.ts +6 -1
- package/src/cli/index.ts +1 -1
- package/src/client/index.ts +119 -0
- package/src/components/Block.astro +12 -0
- package/src/components/CommentForm.astro +1 -1
- package/src/components/Comments.astro +1 -1
- package/src/components/InlinePortableTextEditor.tsx +260 -32
- package/src/components/OrderedList.astro +14 -0
- package/src/components/PortableText.astro +26 -4
- package/src/components/index.ts +12 -1
- package/src/components/ordered-list.ts +549 -0
- package/src/content/converters/mark-safety.ts +120 -0
- package/src/content/converters/numbered-list.ts +148 -0
- package/src/content/converters/portable-text-to-prosemirror.ts +84 -25
- package/src/content/converters/prosemirror-to-portable-text.ts +35 -14
- package/src/content/converters/types.ts +2 -0
- package/src/content/portable-text-lists.ts +205 -0
- package/src/content-list-query.ts +29 -0
- package/src/database/dialect-helpers.ts +3 -22
- package/src/database/migrations/055_content_translation_group_locale_index.ts +67 -0
- package/src/database/migrations/056_taxonomy_term_sort_order.ts +189 -0
- package/src/database/migrations/057_collection_hidden.ts +24 -0
- package/src/database/migrations/058_collection_sort_order.ts +20 -0
- package/src/database/migrations/059_revision_prune_queue.ts +36 -0
- package/src/database/migrations/060_collection_admin_config.ts +16 -0
- package/src/database/migrations/061_media_usage_cleanup.ts +77 -0
- package/src/database/migrations/062_media_usage_cleanup_fence.ts +295 -0
- package/src/database/migrations/063_media_usage_incremental_work.ts +397 -0
- package/src/database/migrations/064_fts_plain_text.ts +253 -0
- package/src/database/migrations/065_media_usage_collection_deletion.ts +85 -0
- package/src/database/migrations/066_media_usage_reconciliation.ts +115 -0
- package/src/database/migrations/067_indexed_content_fields.ts +39 -0
- package/src/database/migrations/068_content_taxonomy_entry_groups.ts +61 -0
- package/src/database/migrations/069_collection_title_date_fields.ts +45 -0
- package/src/database/migrations/070_collection_routable.ts +18 -0
- package/src/database/migrations/runner.ts +32 -0
- package/src/database/repositories/content.ts +1051 -273
- package/src/database/repositories/media-usage-work.ts +848 -0
- package/src/database/repositories/media-usage.ts +1502 -138
- package/src/database/repositories/revision.ts +98 -22
- package/src/database/repositories/taxonomy.ts +429 -103
- package/src/database/repositories/types.ts +58 -2
- package/src/database/types.ts +147 -17
- package/src/database/validate.ts +39 -0
- package/src/db/adapters.ts +36 -0
- package/src/db/index.ts +3 -0
- package/src/deferred-tasks.ts +71 -0
- package/src/emdash-runtime.ts +493 -110
- package/src/fields/file.ts +7 -4
- package/src/fields/image.ts +8 -1
- package/src/fields/types.ts +18 -5
- package/src/i18n/config.ts +32 -0
- package/src/i18n/repair-locale-casing.ts +0 -14
- package/src/i18n/taxonomy-locale-diagnostic.ts +47 -0
- package/src/import/wxr-taxonomies.ts +7 -47
- package/src/index.ts +27 -2
- package/src/loader.ts +165 -89
- package/src/mcp/server.ts +242 -34
- package/src/media/usage/activation.ts +626 -0
- package/src/media/usage/capture-triggers.ts +543 -0
- package/src/media/usage/cleanup.ts +305 -0
- package/src/media/usage/collection-deletion-processor.ts +356 -0
- package/src/media/usage/collection-deletion.ts +836 -0
- package/src/media/usage/content-fields.ts +40 -3
- package/src/media/usage/content-refresh.ts +356 -47
- package/src/media/usage/content-repair.ts +180 -61
- package/src/media/usage/content-snapshots.ts +85 -105
- package/src/media/usage/gc.ts +50 -0
- package/src/media/usage/projection-fingerprint.ts +129 -0
- package/src/media/usage/reconciliation-processor.ts +372 -0
- package/src/media/usage/reconciliation.ts +1117 -0
- package/src/media/usage/source-key.ts +4 -0
- package/src/media/usage/types.ts +2 -0
- package/src/media/usage/work-processor.ts +248 -0
- package/src/menus/index.ts +246 -141
- package/src/object-cache/index.ts +160 -3
- package/src/plugin-types.ts +8 -0
- package/src/plugins/adapt-sandbox-entry.ts +24 -5
- package/src/plugins/context.ts +24 -7
- package/src/plugins/index.ts +9 -0
- package/src/plugins/routes.ts +77 -1
- package/src/plugins/sandbox/index.ts +10 -1
- package/src/plugins/sandbox/types.ts +78 -1
- package/src/plugins/scheduler/node.ts +13 -1
- package/src/plugins/scheduler/types.ts +2 -0
- package/src/plugins/storage-indexes.ts +50 -11
- package/src/plugins/types.ts +39 -2
- package/src/query.ts +50 -79
- package/src/redirects/cache.ts +79 -19
- package/src/request-context.ts +3 -0
- package/src/scheduled-publish.ts +6 -1
- package/src/scheduler-health.ts +46 -0
- package/src/schema/registry.ts +863 -249
- package/src/schema/types.ts +74 -8
- package/src/schema/url-pattern.ts +33 -0
- package/src/schema/zod-generator.ts +77 -5
- package/src/search/fts-manager.ts +167 -86
- package/src/search/index.ts +2 -0
- package/src/search/query.ts +32 -11
- package/src/search/types.ts +8 -0
- package/src/seed/apply.ts +89 -16
- package/src/seed/types.ts +21 -3
- package/src/seed/validate.ts +52 -2
- package/src/taxonomies/index.ts +88 -82
- package/src/taxonomies/term-counts.ts +23 -12
- package/src/ui-comments.ts +19 -0
- package/src/ui.ts +11 -3
- package/src/utils/slugify.ts +1 -29
- package/src/virtual-modules.d.ts +24 -0
- package/dist/adapters-C0ONzkUP.d.mts.map +0 -1
- package/dist/after-B1IIdH3Y.mjs +0 -29
- package/dist/after-B1IIdH3Y.mjs.map +0 -1
- package/dist/api-DgPbMlng.mjs.map +0 -1
- package/dist/apply-1_6ra7NP.mjs.map +0 -1
- package/dist/cache-B26cufFd.mjs +0 -65
- package/dist/cache-B26cufFd.mjs.map +0 -1
- package/dist/config-DyF3OKg_.mjs.map +0 -1
- package/dist/content-CpfKV9QE.mjs.map +0 -1
- package/dist/content-refresh-DgqUBeTv.mjs +0 -1629
- package/dist/content-refresh-DgqUBeTv.mjs.map +0 -1
- package/dist/context-kO4YZB03.mjs.map +0 -1
- package/dist/dashboard-DoBwPnON.mjs.map +0 -1
- package/dist/dialect-helpers-DFwyWjTM.mjs.map +0 -1
- package/dist/errors-DtEXIQQV.mjs.map +0 -1
- package/dist/fts-manager-vncsZIJm.mjs.map +0 -1
- package/dist/index-BeNto_Vs.d.mts.map +0 -1
- package/dist/index-j9rTh3cp.d.mts.map +0 -1
- package/dist/loader-C1XOLV5b.mjs.map +0 -1
- package/dist/media-DWIzoVFL.mjs.map +0 -1
- package/dist/media-allowlist-B7TPD6aq.mjs.map +0 -1
- package/dist/media-usage-DPJcNaAq.mjs.map +0 -1
- package/dist/menus-C6qhqP4X.mjs.map +0 -1
- package/dist/object-cache-Bok5j2ae.mjs.map +0 -1
- package/dist/options-aeIs8WFu.d.mts.map +0 -1
- package/dist/public-url-DSGTnJFw.mjs.map +0 -1
- package/dist/query-DCiXI7OZ.mjs.map +0 -1
- package/dist/redirects-BOn2DIUs.mjs.map +0 -1
- package/dist/registry-BP1JK2xh.mjs +0 -795
- package/dist/registry-BP1JK2xh.mjs.map +0 -1
- package/dist/relations-DHFQGX0h.d.mts.map +0 -1
- package/dist/runner-BsI18UgP.mjs.map +0 -1
- package/dist/search-CeO678cp.mjs.map +0 -1
- package/dist/seo-C3GDfT0V.mjs.map +0 -1
- package/dist/site-url-8N3kSyVZ.mjs +0 -13
- package/dist/site-url-8N3kSyVZ.mjs.map +0 -1
- package/dist/slugify-C_tqlU4G.mjs +0 -30
- package/dist/slugify-C_tqlU4G.mjs.map +0 -1
- package/dist/taxonomies-B61CRSha.mjs.map +0 -1
- package/dist/taxonomies-Dbdki3Un.mjs.map +0 -1
- package/dist/taxonomy-DvwWAPvA.mjs +0 -304
- package/dist/taxonomy-DvwWAPvA.mjs.map +0 -1
- package/dist/types-84Bque3-.d.mts.map +0 -1
- package/dist/types-BQY7VYw6.d.mts.map +0 -1
- package/dist/types-DqZVPILm.d.mts.map +0 -1
- package/dist/types-XrQQ-Aex.mjs.map +0 -1
- package/dist/types-jISEalTX.d.mts.map +0 -1
- package/dist/types-o7xo7VgH.mjs.map +0 -1
- package/dist/validate-Bs_wT2ul.mjs.map +0 -1
- package/dist/validate-CdYcY-Ln.d.mts.map +0 -1
- package/dist/validate-VPnKoIzW.mjs.map +0 -1
- package/dist/version-Bdfb34kk.mjs +0 -7
- package/dist/zod-generator-DMzfga3g.mjs.map +0 -1
- /package/dist/{api-tokens-CEsW_jCg.mjs → api-tokens-CyTsLszZ.mjs} +0 -0
- /package/dist/{ssrf-BB__xfNM.mjs → ssrf-DpXgKsna.mjs} +0 -0
- /package/dist/{types-CK0LxPUQ.mjs → types-CP1CflWf.mjs} +0 -0
|
@@ -1,93 +1,106 @@
|
|
|
1
|
-
import { a as runMigrations, n as MIGRATION_RACE_WAIT_MS, t as ConcurrentMigrationTimeoutError } from "../runner-
|
|
2
|
-
import { t as validateIdentifier } from "../validate-
|
|
3
|
-
import { c as isSqlite,
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { t as after } from "../after-
|
|
1
|
+
import { a as runMigrations, n as MIGRATION_RACE_WAIT_MS, t as ConcurrentMigrationTimeoutError } from "../runner-BiDvq5dv.mjs";
|
|
2
|
+
import { t as validateIdentifier } from "../validate-BpwoDOgt.mjs";
|
|
3
|
+
import { c as isSqlite, p as tableExists, s as isPostgres, u as listTablesLike } from "../dialect-helpers-BSbfaO3F.mjs";
|
|
4
|
+
import { r as getI18nConfig, s as setI18nConfig } from "../config-C3EBstHi.mjs";
|
|
5
|
+
import { C as createHookPipeline, M as AuditRepository, T as definePlugin, _ as PluginRouteRegistry, b as toRouteCallerInfo, c as getSandboxRouteErrorDetails, t as getMenu, v as buildRouteMeta, w as resolveExclusiveHooks, x as EmailPipeline, y as parseRouteInput } from "../menus-CC-yn1F9.mjs";
|
|
6
|
+
import { n as createDeferredTaskTracker, t as after } from "../after-CBU2vkoO.mjs";
|
|
7
7
|
import { createRequestMetrics, getRequestContext, runWithContext } from "../request-context.mjs";
|
|
8
|
-
import "../object-cache-
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import "../
|
|
12
|
-
import "../
|
|
13
|
-
import
|
|
14
|
-
import "../
|
|
15
|
-
import "../
|
|
16
|
-
import "../
|
|
17
|
-
import { t as
|
|
18
|
-
import
|
|
19
|
-
import "../
|
|
20
|
-
import "../
|
|
8
|
+
import { o as getLastContentWriteAt } from "../object-cache-71tPfV4A.mjs";
|
|
9
|
+
import { i as MAX_COLLECTION_LIST_COLUMNS } from "../types-75KIqzL9.mjs";
|
|
10
|
+
import { i as RevisionRepository, t as ContentRepository } from "../content-BNfwh7xX.mjs";
|
|
11
|
+
import { n as isMissingTableError } from "../db-errors-Cl-sKYTm.mjs";
|
|
12
|
+
import "../slugify-B994zkXC.mjs";
|
|
13
|
+
import "../base64-CvLhUKQv.mjs";
|
|
14
|
+
import { n as ContentMutationConflictError } from "../types-CQPWhPxE.mjs";
|
|
15
|
+
import { t as MediaRepository } from "../media-CzhAn2--.mjs";
|
|
16
|
+
import "../user-sHgJ62SP.mjs";
|
|
17
|
+
import { t as withTransaction } from "../transaction-NQj4VJ7Z.mjs";
|
|
18
|
+
import "../taxonomy-CPeM8d2o.mjs";
|
|
19
|
+
import "../comment-Dxlep6cE.mjs";
|
|
20
|
+
import { t as OptionsRepository } from "../options-CBXIHUoT.mjs";
|
|
21
|
+
import { r as createSiteInfo } from "../context-DRgOvSnW.mjs";
|
|
22
|
+
import "../menus-fxvEDYE6.mjs";
|
|
23
|
+
import "../redirect-COsS6SWJ.mjs";
|
|
21
24
|
import { i as setRequestCacheEntry, r as requestCached } from "../request-cache-BSUptuJR.mjs";
|
|
22
|
-
import "../byline-registry-
|
|
23
|
-
import { n as initWithLock, t as createInitLock } from "../init-lock-
|
|
24
|
-
import "../field-defs-cache-
|
|
25
|
-
import "../byline-
|
|
26
|
-
import "../relation-
|
|
27
|
-
import { n as normalizeMediaValue } from "../normalize-
|
|
28
|
-
import "../
|
|
29
|
-
import "../
|
|
30
|
-
import
|
|
31
|
-
import "../
|
|
25
|
+
import "../byline-registry-DDun8sxh.mjs";
|
|
26
|
+
import { n as initWithLock, t as createInitLock } from "../init-lock-6b309ZrF.mjs";
|
|
27
|
+
import "../field-defs-cache-hg6QkjV9.mjs";
|
|
28
|
+
import "../byline-pEcoDps8.mjs";
|
|
29
|
+
import "../relation-DtmAQH7s.mjs";
|
|
30
|
+
import { n as normalizeMediaValue } from "../normalize-Bc-s_r-a.mjs";
|
|
31
|
+
import "../dist-D_6CKR8z.mjs";
|
|
32
|
+
import "../enrich-_ySMEvEE.mjs";
|
|
33
|
+
import "../seo-CvDxfvFb.mjs";
|
|
34
|
+
import { r as singleFlightCached, t as createSingleFlightCache } from "../single-flight-cache-B0eLKS6K.mjs";
|
|
35
|
+
import "../cache-CBJaDy_E.mjs";
|
|
36
|
+
import { t as FTSManager } from "../fts-manager-CZ-5dVwJ.mjs";
|
|
37
|
+
import "../resolve-DOhVEvty.mjs";
|
|
32
38
|
import { createRecorder, flushRecorder, isInstrumentationEnabled, kyselyLogOption } from "../database/instrumentation.mjs";
|
|
33
|
-
import {
|
|
34
|
-
import { a as getTaxonomyDefs, o as getTaxonomyTerms } from "../taxonomies-
|
|
35
|
-
import { $ as
|
|
36
|
-
import "../dashboard-
|
|
37
|
-
import { r as hashString } from "../hash-
|
|
38
|
-
import { a as findNonTranslatableSiblingContentIds,
|
|
39
|
-
import "../media-usage-
|
|
40
|
-
import
|
|
41
|
-
import "../
|
|
42
|
-
import "../
|
|
43
|
-
import "../
|
|
44
|
-
import "../
|
|
45
|
-
import
|
|
46
|
-
import "../
|
|
47
|
-
import
|
|
48
|
-
import
|
|
49
|
-
import "../
|
|
50
|
-
import "../
|
|
51
|
-
import {
|
|
52
|
-
import "../
|
|
53
|
-
import "../
|
|
54
|
-
import "../
|
|
55
|
-
import "../
|
|
56
|
-
import "../
|
|
57
|
-
import "../
|
|
58
|
-
import "../
|
|
59
|
-
import "../
|
|
60
|
-
import
|
|
61
|
-
import "../
|
|
62
|
-
import
|
|
63
|
-
import {
|
|
64
|
-
import "../
|
|
65
|
-
import {
|
|
66
|
-
import {
|
|
67
|
-
import "../
|
|
68
|
-
import "../
|
|
69
|
-
import "../
|
|
70
|
-
import "../
|
|
71
|
-
import "../
|
|
72
|
-
import "../
|
|
73
|
-
import
|
|
74
|
-
import "../
|
|
75
|
-
import "../
|
|
76
|
-
import "../
|
|
77
|
-
import "../
|
|
39
|
+
import { s as getDb } from "../loader-DLBqRiGP.mjs";
|
|
40
|
+
import { a as getTaxonomyDefs, o as getTaxonomyTerms } from "../taxonomies-B0o9ExNV.mjs";
|
|
41
|
+
import { $ as handleContentUnschedule, A as handleMediaUpdate, B as handleContentDelete, D as handleMediaDelete, E as handleMediaCreate, F as handleContentAuthors, G as handleContentList, H as handleContentDuplicate, I as handleContentCompare, J as handleContentPublish, K as handleContentListTrashed, L as handleContentCountScheduled, M as handleRevisionList, N as handleRevisionRestore, O as handleMediaGet, Q as handleContentUnpublish, R as handleContentCountTrashed, U as handleContentGet, V as handleContentDiscardDraft, W as handleContentGetIncludingTrashed, X as handleContentSchedule, Y as handleContentRestore, Z as handleContentTranslations, _ as loadBundleFromR2, et as handleContentUpdate, j as handleRevisionGet, k as handleMediaList, q as handleContentPermanentDelete, s as normalizeRegistryConfig, tt as validateRev, v as syncDeclaredStorageIndexes, z as handleContentCreate } from "../api-DdAsirBe.mjs";
|
|
42
|
+
import { n as recordSchedulerHeartbeatSafely } from "../dashboard-CB8J89rJ.mjs";
|
|
43
|
+
import { r as hashString } from "../hash-Cg0TXJPV.mjs";
|
|
44
|
+
import { _ as loadContentMediaUsageFields, a as findNonTranslatableSiblingContentIds, g as buildContentMediaUsageFieldFingerprint, l as refreshContentMediaUsageAfterWrite, r as deleteContentMediaUsage, s as markContentMediaUsageCollectionStale, u as refreshContentMediaUsageForWork, v as CONTENT_SOURCE_SCHEMA_VERSION, y as MediaUsageRepository } from "../content-refresh-DNERP4-f.mjs";
|
|
45
|
+
import { t as MediaUsageWorkRepository } from "../media-usage-work-DrCbqBcl.mjs";
|
|
46
|
+
import "../media-usage-De3qvwjc.mjs";
|
|
47
|
+
import { n as collectionDeletionCurrentTimestamp, r as deleteActivatedMediaUsageCollection, t as MediaUsageCollectionDeletionRepository } from "../collection-deletion-C8BugSGz.mjs";
|
|
48
|
+
import "../media-usage-work-B9Bksh_b.mjs";
|
|
49
|
+
import { n as SchemaRegistry } from "../registry-C581V4TP.mjs";
|
|
50
|
+
import "../query-vgZNwIhh.mjs";
|
|
51
|
+
import "../zod-generator-D11MhxG4.mjs";
|
|
52
|
+
import "../schema-BFyTg38T.mjs";
|
|
53
|
+
import "../schema-CeQSNwB3.mjs";
|
|
54
|
+
import "../seo-AFpVvTqr.mjs";
|
|
55
|
+
import { t as PluginStateRepository } from "../state-DYwPue40.mjs";
|
|
56
|
+
import "../sections-COctk6hf.mjs";
|
|
57
|
+
import { i as getSiteSettings, o as invalidateSiteSettingsCache } from "../settings-B372MV0p.mjs";
|
|
58
|
+
import "../settings-BE9aqn_9.mjs";
|
|
59
|
+
import "../taxonomies-Dk3MAklL.mjs";
|
|
60
|
+
import { r as normalizeManifestRoute } from "../manifest-schema-BXbUAHod.mjs";
|
|
61
|
+
import "../types-CP1CflWf.mjs";
|
|
62
|
+
import "../ssrf-Cpvg_n3A.mjs";
|
|
63
|
+
import "../error-DupyJZE2.mjs";
|
|
64
|
+
import "../parse-DFbuc-Fh.mjs";
|
|
65
|
+
import "../media-BWWWb8kg.mjs";
|
|
66
|
+
import "../redirects-cV9g7V-0.mjs";
|
|
67
|
+
import "../relations-BBApI4e2.mjs";
|
|
68
|
+
import "../status-CWVYpbD8.mjs";
|
|
69
|
+
import { a as invalidateUrlPatternCache } from "../query-BgjfiLUQ.mjs";
|
|
70
|
+
import "../import-B41u8Rot.mjs";
|
|
71
|
+
import { t as getTrustedProxyHeaders } from "../trusted-proxy-CEVekpby.mjs";
|
|
72
|
+
import { n as sanitizeHeadersForSandbox, t as extractRequestMeta } from "../request-meta-DgZJh3zG.mjs";
|
|
73
|
+
import "../ssrf-DpXgKsna.mjs";
|
|
74
|
+
import { n as CronExecutor } from "../cron-Y9aLtgrA.mjs";
|
|
75
|
+
import { r as devConsoleEmailDeliver, t as DEV_CONSOLE_EMAIL_PLUGIN_ID } from "../email-console-BHNjIwiB.mjs";
|
|
76
|
+
import { n as assertMediaUsageActivationWriteAllowed } from "../media-usage-write-fence-aHEgN1zB.mjs";
|
|
77
|
+
import "../utils-D4vD_Y98.mjs";
|
|
78
|
+
import "../tokens-C39klX8R.mjs";
|
|
79
|
+
import "../preview-PW-nOnq4.mjs";
|
|
80
|
+
import "../hreflang-DNswoYsi.mjs";
|
|
81
|
+
import "../comment-reaction-D6qfHKBc.mjs";
|
|
82
|
+
import "../bylines-BWemdX2k.mjs";
|
|
83
|
+
import { n as getWidgetAreas } from "../widgets-Cwzvww0W.mjs";
|
|
84
|
+
import "../validate-BkqIG_Uj.mjs";
|
|
85
|
+
import "../apply-D5zfeD09.mjs";
|
|
86
|
+
import "../load-DmvJrtxS.mjs";
|
|
87
|
+
import "../search-BpuPKZb0.mjs";
|
|
78
88
|
import "../index.mjs";
|
|
79
|
-
import { n as VERSION, t as COMMIT } from "../version-
|
|
80
|
-
import { t as getAuthMode } from "../mode-
|
|
81
|
-
import "../snapshot-
|
|
82
|
-
import { u as maybeRunScheduledBackup } from "../backup-
|
|
83
|
-
import { t as cleanupExpiredChallenges } from "../challenge-store-
|
|
84
|
-
import { t as removeUploadAttempt } from "../upload-attempts-
|
|
85
|
-
import { a as validateEncryptionKeyAtStartup } from "../secrets-
|
|
86
|
-
import {
|
|
89
|
+
import { n as VERSION, t as COMMIT } from "../version-x0NjTScG.mjs";
|
|
90
|
+
import { t as getAuthMode } from "../mode-Bpe-tNNy.mjs";
|
|
91
|
+
import "../snapshot-B5h6aPMh.mjs";
|
|
92
|
+
import { u as maybeRunScheduledBackup } from "../backup-LHjIAVNP.mjs";
|
|
93
|
+
import { t as cleanupExpiredChallenges } from "../challenge-store-CZyFVaad.mjs";
|
|
94
|
+
import { t as removeUploadAttempt } from "../upload-attempts-hlne3iA1.mjs";
|
|
95
|
+
import { a as validateEncryptionKeyAtStartup } from "../secrets-dtYs2hDo.mjs";
|
|
96
|
+
import { isRecord } from "../plugin-utils.mjs";
|
|
97
|
+
import { t as resolveSessionUser } from "../session-user-BzVemLkw.mjs";
|
|
87
98
|
import { Kysely, sql } from "kysely";
|
|
99
|
+
import { ulid } from "ulidx";
|
|
88
100
|
import { z } from "zod";
|
|
89
101
|
import { Permissions } from "@emdash-cms/auth";
|
|
90
102
|
import { defineMiddleware } from "astro:middleware";
|
|
103
|
+
import { buildTime } from "virtual:emdash/build";
|
|
91
104
|
import virtualConfig from "virtual:emdash/config";
|
|
92
105
|
import { createCoalescingDialect, createDialect, createRequestScopedDb } from "virtual:emdash/dialect";
|
|
93
106
|
import { mediaProviders } from "virtual:emdash/media-providers";
|
|
@@ -104,9 +117,7 @@ async function repairLocaleCasing(db, configuredLocales) {
|
|
|
104
117
|
const tableNames = await listTablesLike(db, "ec_%");
|
|
105
118
|
for (const tableName of tableNames) {
|
|
106
119
|
const table = sql.ref(tableName);
|
|
107
|
-
const
|
|
108
|
-
for (const locale of configuredLocales) {
|
|
109
|
-
await sql`
|
|
120
|
+
for (const locale of configuredLocales) await sql`
|
|
110
121
|
UPDATE ${table} AS target
|
|
111
122
|
SET locale = ${locale}
|
|
112
123
|
WHERE lower(target.locale) = lower(${locale})
|
|
@@ -124,20 +135,1165 @@ async function repairLocaleCasing(db, configuredLocales) {
|
|
|
124
135
|
AND candidate.locale != ${locale}
|
|
125
136
|
)
|
|
126
137
|
`.execute(db);
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
//#endregion
|
|
142
|
+
//#region src/i18n/taxonomy-locale-diagnostic.ts
|
|
143
|
+
const REPAIR_GUIDE = "https://docs.emdashcms.com/guides/internationalization/#repairing-taxonomy-locale-mismatches";
|
|
144
|
+
async function warnAboutUnconfiguredTaxonomyLocales(db, configuredLocales, definitionLocales) {
|
|
145
|
+
const supportedLocales = configuredLocales.length > 0 ? configuredLocales : ["en"];
|
|
146
|
+
const definitionRows = definitionLocales === void 0 ? await db.selectFrom("_emdash_taxonomy_defs").select("locale").distinct().where("locale", "not in", supportedLocales).execute() : [...new Set(definitionLocales)].filter((locale) => !supportedLocales.includes(locale)).map((locale) => ({ locale }));
|
|
147
|
+
const termRows = await db.selectFrom("taxonomies").select("locale").distinct().where("locale", "not in", supportedLocales).execute();
|
|
148
|
+
const mismatches = [...definitionRows.map(({ locale }) => ({
|
|
149
|
+
source: "definitions",
|
|
150
|
+
locale
|
|
151
|
+
})), ...termRows.map(({ locale }) => ({
|
|
152
|
+
source: "terms",
|
|
153
|
+
locale
|
|
154
|
+
}))].toSorted((a, b) => a.source.localeCompare(b.source) || a.locale.localeCompare(b.locale));
|
|
155
|
+
if (mismatches.length === 0) return;
|
|
156
|
+
const details = mismatches.map(({ source, locale }) => `${source}: ${locale}`).join("; ");
|
|
157
|
+
console.warn(`EmDash: Taxonomy rows use locales outside the configured locales (${supportedLocales.join(", ")}): ${details}. Locale-scoped reads may not return these rows. Review and repair them explicitly: ${REPAIR_GUIDE}`);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
//#endregion
|
|
161
|
+
//#region src/media/usage/collection-deletion-processor.ts
|
|
162
|
+
const MEDIA_USAGE_COLLECTION_DELETION_LIMITS = Object.freeze({
|
|
163
|
+
candidatesPerTick: 4,
|
|
164
|
+
deletionsPerTick: 1,
|
|
165
|
+
rowsPerBatch: 50,
|
|
166
|
+
leaseDurationSeconds: 300,
|
|
167
|
+
maxAttempts: 5,
|
|
168
|
+
retryBaseSeconds: 30,
|
|
169
|
+
retryMaxSeconds: 900,
|
|
170
|
+
maxQueriesPerTick: 30
|
|
171
|
+
});
|
|
172
|
+
async function processDueMediaUsageCollectionDeletions(db) {
|
|
173
|
+
const repository = new MediaUsageCollectionDeletionRepository(db);
|
|
174
|
+
const candidates = await repository.findDue(MEDIA_USAGE_COLLECTION_DELETION_LIMITS.candidatesPerTick);
|
|
175
|
+
if (candidates.length === 0) return {
|
|
176
|
+
candidateCount: 0,
|
|
177
|
+
claimedCount: 0,
|
|
178
|
+
outcome: "idle"
|
|
179
|
+
};
|
|
180
|
+
let claim = null;
|
|
181
|
+
for (const candidate of candidates) {
|
|
182
|
+
claim = await repository.claim({
|
|
183
|
+
collectionId: candidate.collectionId,
|
|
184
|
+
phase: candidate.phase,
|
|
185
|
+
leaseDurationSeconds: MEDIA_USAGE_COLLECTION_DELETION_LIMITS.leaseDurationSeconds
|
|
186
|
+
});
|
|
187
|
+
if (claim?.leaseToken) break;
|
|
188
|
+
}
|
|
189
|
+
if (!claim?.leaseToken) return {
|
|
190
|
+
candidateCount: candidates.length,
|
|
191
|
+
claimedCount: 0,
|
|
192
|
+
outcome: "claim_lost"
|
|
193
|
+
};
|
|
194
|
+
try {
|
|
195
|
+
const processed = await processClaimedDeletion(db, claim);
|
|
196
|
+
if (!processed.finalized && !processed.released && !await repository.release(claim)) return {
|
|
197
|
+
candidateCount: candidates.length,
|
|
198
|
+
claimedCount: 1,
|
|
199
|
+
outcome: "claim_lost"
|
|
200
|
+
};
|
|
201
|
+
return {
|
|
202
|
+
candidateCount: candidates.length,
|
|
203
|
+
claimedCount: 1,
|
|
204
|
+
outcome: processed.finalized ? "finalized" : "progress"
|
|
205
|
+
};
|
|
206
|
+
} catch (error) {
|
|
207
|
+
const terminal = claim.attemptCount + 1 >= MEDIA_USAGE_COLLECTION_DELETION_LIMITS.maxAttempts;
|
|
208
|
+
if (!await repository.recordFailure({
|
|
209
|
+
collectionId: claim.collectionId,
|
|
210
|
+
leaseToken: claim.leaseToken,
|
|
211
|
+
errorCode: "MEDIA_USAGE_COLLECTION_DELETION_FAILED",
|
|
212
|
+
terminal,
|
|
213
|
+
retryDelaySeconds: retryDelaySeconds$2(claim.attemptCount)
|
|
214
|
+
})) return {
|
|
215
|
+
candidateCount: candidates.length,
|
|
216
|
+
claimedCount: 1,
|
|
217
|
+
outcome: "claim_lost"
|
|
218
|
+
};
|
|
219
|
+
console.error("[media-usage:collection-deletion] Processing failed:", error);
|
|
220
|
+
return {
|
|
221
|
+
candidateCount: candidates.length,
|
|
222
|
+
claimedCount: 1,
|
|
223
|
+
outcome: terminal ? "failed" : "retry"
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
async function processClaimedDeletion(db, claim) {
|
|
228
|
+
if (claim.phase === "fence" || claim.phase === "registry" || claim.phase === "table") {
|
|
229
|
+
await deleteActivatedMediaUsageCollection(db, {
|
|
230
|
+
collectionId: claim.collectionId,
|
|
231
|
+
collectionSlug: claim.collectionSlug,
|
|
232
|
+
forceDelete: claim.forceDelete
|
|
233
|
+
}, {
|
|
234
|
+
frontPhaseLimit: 1,
|
|
235
|
+
claimed: claim
|
|
236
|
+
});
|
|
237
|
+
return {
|
|
238
|
+
finalized: false,
|
|
239
|
+
released: true
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
if (claim.phase === "work") await processWorkBatch(db, claim);
|
|
243
|
+
if (claim.phase === "sources") await processSourceBatch(db, claim);
|
|
244
|
+
if (claim.phase === "status") await processStatus(db, claim);
|
|
245
|
+
if (claim.phase === "finalize") {
|
|
246
|
+
await finalizeDeletion(db, claim);
|
|
247
|
+
return {
|
|
248
|
+
finalized: true,
|
|
249
|
+
released: true
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
return {
|
|
253
|
+
finalized: false,
|
|
254
|
+
released: false
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
async function processWorkBatch(db, claim) {
|
|
258
|
+
await withTransaction(db, async (trx) => {
|
|
259
|
+
const rows = await trx.selectFrom("_emdash_media_usage_work").select("content_id").where("collection_id", "=", claim.collectionId).$if(claim.workCursor !== null, (query) => query.where("content_id", ">", claim.workCursor)).orderBy("content_id", "asc").limit(MEDIA_USAGE_COLLECTION_DELETION_LIMITS.rowsPerBatch + 1).execute();
|
|
260
|
+
const batch = rows.slice(0, MEDIA_USAGE_COLLECTION_DELETION_LIMITS.rowsPerBatch);
|
|
261
|
+
if (batch.length > 0) await trx.deleteFrom("_emdash_media_usage_work").where("collection_id", "=", claim.collectionId).where("content_id", "in", batch.map((row) => row.content_id)).where(liveLeaseGuard(trx, claim)).execute();
|
|
262
|
+
await updateDeletion(trx, claim, {
|
|
263
|
+
phase: rows.length > MEDIA_USAGE_COLLECTION_DELETION_LIMITS.rowsPerBatch ? "work" : "sources",
|
|
264
|
+
work_cursor: rows.length > MEDIA_USAGE_COLLECTION_DELETION_LIMITS.rowsPerBatch ? batch.at(-1).content_id : null
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
return false;
|
|
268
|
+
}
|
|
269
|
+
async function processSourceBatch(db, claim) {
|
|
270
|
+
await withTransaction(db, async (trx) => {
|
|
271
|
+
let sourceKey = claim.sourceKey;
|
|
272
|
+
if (!sourceKey) {
|
|
273
|
+
const source = await trx.selectFrom("_emdash_media_usage_sources").select("source_key").where("source_type", "=", "content").where("collection_id", "=", claim.collectionId).orderBy("source_key", "asc").limit(1).executeTakeFirst();
|
|
274
|
+
if (!source) {
|
|
275
|
+
await updateDeletion(trx, claim, {
|
|
276
|
+
phase: "status",
|
|
277
|
+
source_key: null,
|
|
278
|
+
occurrence_cursor: null
|
|
279
|
+
});
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
sourceKey = source.source_key;
|
|
283
|
+
await updateDeletion(trx, claim, {
|
|
284
|
+
source_key: sourceKey,
|
|
285
|
+
occurrence_cursor: null
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
const occurrences = await trx.selectFrom("_emdash_media_usage").select("id").where("source_key", "=", sourceKey).$if(claim.occurrenceCursor !== null, (query) => query.where("id", ">", claim.occurrenceCursor)).orderBy("id", "asc").limit(MEDIA_USAGE_COLLECTION_DELETION_LIMITS.rowsPerBatch + 1).execute();
|
|
289
|
+
const batch = occurrences.slice(0, MEDIA_USAGE_COLLECTION_DELETION_LIMITS.rowsPerBatch);
|
|
290
|
+
if (batch.length > 0) await trx.deleteFrom("_emdash_media_usage").where("source_key", "=", sourceKey).where("id", "in", batch.map((row) => row.id)).where(liveLeaseGuard(trx, claim)).execute();
|
|
291
|
+
if (occurrences.length > MEDIA_USAGE_COLLECTION_DELETION_LIMITS.rowsPerBatch) {
|
|
292
|
+
await updateDeletion(trx, claim, {
|
|
293
|
+
source_key: sourceKey,
|
|
294
|
+
occurrence_cursor: batch.at(-1).id
|
|
295
|
+
});
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
await trx.deleteFrom("_emdash_media_usage_sources").where("source_key", "=", sourceKey).where("source_type", "=", "content").where("collection_id", "=", claim.collectionId).where(liveLeaseGuard(trx, claim)).execute();
|
|
299
|
+
await updateDeletion(trx, claim, {
|
|
300
|
+
source_key: null,
|
|
301
|
+
occurrence_cursor: null
|
|
302
|
+
});
|
|
303
|
+
});
|
|
304
|
+
return false;
|
|
305
|
+
}
|
|
306
|
+
async function processStatus(db, claim) {
|
|
307
|
+
await withTransaction(db, async (trx) => {
|
|
308
|
+
if (await exactCleanupRowsRemain(trx, claim, false)) throw new Error("Collection deletion cleanup is incomplete");
|
|
309
|
+
await trx.deleteFrom("_emdash_media_usage_reconciliations").where("collection_id", "=", claim.collectionId).where("collection_slug", "=", claim.collectionSlug).where(liveLeaseGuard(trx, claim)).execute();
|
|
310
|
+
await trx.deleteFrom("_emdash_media_usage_index_status").where("adapter_id", "=", "content-media").where("scope_type", "=", "collection").where("scope_key", "=", claim.collectionSlug).where("collection_id", "=", claim.collectionId).where(liveLeaseGuard(trx, claim)).execute();
|
|
311
|
+
await updateDeletion(trx, claim, { phase: "finalize" });
|
|
312
|
+
});
|
|
313
|
+
return false;
|
|
314
|
+
}
|
|
315
|
+
async function finalizeDeletion(db, claim) {
|
|
316
|
+
if (await tableExists(db, `ec_${claim.collectionSlug}`)) throw new Error("Collection table still exists during deletion finalization");
|
|
317
|
+
if (await exactCleanupRowsRemain(db, claim)) throw new Error("Collection deletion is incomplete");
|
|
318
|
+
if (await db.selectFrom("_emdash_collections").select("id").where("id", "=", claim.collectionId).where("slug", "=", claim.collectionSlug).executeTakeFirst()) throw new Error("Collection registry identity still exists");
|
|
319
|
+
const result = await db.deleteFrom("_emdash_media_usage_collection_deletions").where("collection_id", "=", claim.collectionId).where("collection_slug", "=", claim.collectionSlug).where("state", "=", "leased").where("phase", "=", "finalize").where("lease_token", "=", claim.leaseToken).where(liveLeaseGuard(db, claim)).executeTakeFirst();
|
|
320
|
+
if (Number(result.numDeletedRows ?? 0) !== 1) throw new Error("Collection deletion lost finalization");
|
|
321
|
+
return true;
|
|
322
|
+
}
|
|
323
|
+
async function exactCleanupRowsRemain(db, claim, includeStatus = true) {
|
|
324
|
+
const row = (await sql`
|
|
325
|
+
SELECT
|
|
326
|
+
EXISTS (
|
|
327
|
+
SELECT 1 FROM _emdash_media_usage_work
|
|
328
|
+
WHERE collection_id = ${claim.collectionId}
|
|
329
|
+
) AS work_present,
|
|
330
|
+
EXISTS (
|
|
331
|
+
SELECT 1 FROM _emdash_media_usage_sources
|
|
332
|
+
WHERE source_type = 'content' AND collection_id = ${claim.collectionId}
|
|
333
|
+
) AS source_present,
|
|
334
|
+
EXISTS (
|
|
335
|
+
SELECT 1 FROM _emdash_media_usage_index_status
|
|
336
|
+
WHERE adapter_id = 'content-media'
|
|
337
|
+
AND scope_type = 'collection'
|
|
338
|
+
AND scope_key = ${claim.collectionSlug}
|
|
339
|
+
AND collection_id = ${claim.collectionId}
|
|
340
|
+
) AS status_present
|
|
341
|
+
`.execute(db)).rows[0];
|
|
342
|
+
return Boolean(row?.work_present) || Boolean(row?.source_present) || includeStatus && Boolean(row?.status_present);
|
|
343
|
+
}
|
|
344
|
+
async function updateDeletion(db, claim, values) {
|
|
345
|
+
const result = await db.updateTable("_emdash_media_usage_collection_deletions").set({
|
|
346
|
+
...values,
|
|
347
|
+
attempt_count: 0,
|
|
348
|
+
last_error_code: null,
|
|
349
|
+
updated_at: collectionDeletionCurrentTimestamp(db)
|
|
350
|
+
}).where("collection_id", "=", claim.collectionId).where("state", "=", "leased").where("lease_token", "=", claim.leaseToken).where(liveLeaseGuard(db, claim)).executeTakeFirst();
|
|
351
|
+
if (Number(result.numUpdatedRows ?? 0) !== 1) throw new Error("Collection deletion lease was lost");
|
|
352
|
+
}
|
|
353
|
+
function liveLeaseGuard(db, claim) {
|
|
354
|
+
return isPostgres(db) ? sql`EXISTS (
|
|
355
|
+
SELECT 1 FROM _emdash_media_usage_collection_deletions AS deletion
|
|
356
|
+
WHERE deletion.collection_id = ${claim.collectionId}
|
|
357
|
+
AND deletion.state = 'leased'
|
|
358
|
+
AND deletion.lease_token = ${claim.leaseToken}
|
|
359
|
+
AND deletion.lease_expires_at::timestamptz > clock_timestamp()
|
|
360
|
+
)` : sql`EXISTS (
|
|
361
|
+
SELECT 1 FROM _emdash_media_usage_collection_deletions AS deletion
|
|
362
|
+
WHERE deletion.collection_id = ${claim.collectionId}
|
|
363
|
+
AND deletion.state = 'leased'
|
|
364
|
+
AND deletion.lease_token = ${claim.leaseToken}
|
|
365
|
+
AND deletion.lease_expires_at > strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
|
|
366
|
+
)`;
|
|
367
|
+
}
|
|
368
|
+
function retryDelaySeconds$2(attemptCount) {
|
|
369
|
+
return Math.min(MEDIA_USAGE_COLLECTION_DELETION_LIMITS.retryMaxSeconds, MEDIA_USAGE_COLLECTION_DELETION_LIMITS.retryBaseSeconds * 2 ** attemptCount);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
//#endregion
|
|
373
|
+
//#region src/media/usage/reconciliation.ts
|
|
374
|
+
const ACTIVATION_KEY = "incremental_capture";
|
|
375
|
+
const CONTENT_ADAPTER_ID = "content-media";
|
|
376
|
+
const COLLECTION_SCOPE = "collection";
|
|
377
|
+
const MAX_CANDIDATES = 100;
|
|
378
|
+
const MAX_PORTABLE_DURATION_SECONDS = 365 * 24 * 60 * 60;
|
|
379
|
+
const STABLE_ERROR_CODE_PATTERN = /^[A-Z][A-Z0-9_]{0,63}$/;
|
|
380
|
+
var MediaUsageReconciliationRepository = class {
|
|
381
|
+
constructor(db) {
|
|
382
|
+
this.db = db;
|
|
383
|
+
}
|
|
384
|
+
async findByIdentity(collectionId, runToken) {
|
|
385
|
+
assertIdentity({
|
|
386
|
+
collectionId,
|
|
387
|
+
runToken
|
|
388
|
+
});
|
|
389
|
+
const row = await this.db.selectFrom("_emdash_media_usage_reconciliations").selectAll().where("collection_id", "=", collectionId).where("run_token", "=", runToken).executeTakeFirst();
|
|
390
|
+
return row ? rowToRecord(row) : null;
|
|
391
|
+
}
|
|
392
|
+
async beginRun(claim) {
|
|
393
|
+
const now = timestampOffset(this.db, 0);
|
|
394
|
+
const sameRun = sql`status = 'running' AND cursor = ${claim.runToken}`;
|
|
395
|
+
return (await this.db.updateTable("_emdash_media_usage_index_status as status").set({
|
|
396
|
+
status: "running",
|
|
397
|
+
started_at: sql`CASE WHEN ${sameRun} THEN started_at ELSE ${now} END`,
|
|
398
|
+
completed_at: null,
|
|
399
|
+
cursor: claim.runToken,
|
|
400
|
+
indexed_source_count: 0,
|
|
401
|
+
failed_source_count: 0,
|
|
402
|
+
last_error_code: null,
|
|
403
|
+
change_epoch: sql`CASE WHEN ${sameRun} THEN change_epoch ELSE change_epoch + 1 END`,
|
|
404
|
+
reconciliation_required: 1,
|
|
405
|
+
updated_at: now
|
|
406
|
+
}).where("status.adapter_id", "=", CONTENT_ADAPTER_ID).where("status.scope_type", "=", COLLECTION_SCOPE).where("status.collection_id", "=", claim.collectionId).where("status.scope_key", "=", claim.collectionSlug).where("status.capture_state", "=", "active").where("status.reconciliation_required", "=", 1).where((eb) => eb.or([eb("status.status", "!=", "running"), eb("status.cursor", "=", claim.runToken)])).where(this.liveClaimExists(claim)).returning("change_epoch").executeTakeFirst())?.change_epoch ?? null;
|
|
407
|
+
}
|
|
408
|
+
async findScanUpperId(claim) {
|
|
409
|
+
const tableName = contentTableName(claim.collectionSlug);
|
|
410
|
+
return (await sql`
|
|
411
|
+
SELECT content.id
|
|
412
|
+
FROM ${sql.ref(tableName)} AS content
|
|
413
|
+
WHERE ${this.liveClaimExistsSql(claim)}
|
|
414
|
+
ORDER BY content.id DESC
|
|
415
|
+
LIMIT 1
|
|
416
|
+
`.execute(this.db)).rows[0]?.id ?? null;
|
|
417
|
+
}
|
|
418
|
+
async initializeScan(input) {
|
|
419
|
+
const result = await this.db.updateTable("_emdash_media_usage_reconciliations as reconciliation").set({
|
|
420
|
+
target_epoch: input.targetEpoch,
|
|
421
|
+
field_fingerprint: input.fieldFingerprint,
|
|
422
|
+
phase: "scan",
|
|
423
|
+
scan_cursor: null,
|
|
424
|
+
scan_upper_id: input.scanUpperId,
|
|
425
|
+
source_cursor: null,
|
|
426
|
+
source_upper_key: null,
|
|
427
|
+
attempt_count: 0,
|
|
428
|
+
last_error_code: null,
|
|
429
|
+
updated_at: timestampOffset(this.db, 0)
|
|
430
|
+
}).where("reconciliation.collection_id", "=", input.claim.collectionId).where("reconciliation.run_token", "=", input.claim.runToken).where("reconciliation.target_epoch", "is", null).where("reconciliation.state", "=", "leased").where("reconciliation.lease_token", "=", input.claim.leaseToken).where(liveLease(this.db)).where(this.statusOwnsRun(input.claim, input.targetEpoch)).executeTakeFirst();
|
|
431
|
+
return Number(result.numUpdatedRows ?? 0) === 1;
|
|
432
|
+
}
|
|
433
|
+
async findScanPage(reconciliation, limit) {
|
|
434
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 50) throw new Error("Reconciliation scan page limit must be from 1 to 50");
|
|
435
|
+
if (!reconciliation.leaseToken || reconciliation.targetEpoch === null) return [];
|
|
436
|
+
const tableName = contentTableName(reconciliation.collectionSlug);
|
|
437
|
+
const lowerBound = reconciliation.scanCursor ? sql`AND content.id > ${reconciliation.scanCursor}` : sql``;
|
|
438
|
+
const upperBound = reconciliation.scanUpperId ? sql`AND content.id <= ${reconciliation.scanUpperId}` : sql`AND 1 = 0`;
|
|
439
|
+
return (await sql`
|
|
440
|
+
SELECT content.id
|
|
441
|
+
FROM ${sql.ref(tableName)} AS content
|
|
442
|
+
WHERE 1 = 1
|
|
443
|
+
${lowerBound}
|
|
444
|
+
${upperBound}
|
|
445
|
+
AND ${this.liveClaimExistsSql(reconciliation)}
|
|
446
|
+
ORDER BY content.id ASC
|
|
447
|
+
LIMIT ${limit}
|
|
448
|
+
`.execute(this.db)).rows.map((row) => row.id);
|
|
449
|
+
}
|
|
450
|
+
async checkpointScan(input) {
|
|
451
|
+
let query = this.db.updateTable("_emdash_media_usage_reconciliations as reconciliation").set({
|
|
452
|
+
scan_cursor: input.nextCursor,
|
|
453
|
+
attempt_count: 0,
|
|
454
|
+
last_error_code: null,
|
|
455
|
+
updated_at: timestampOffset(this.db, 0)
|
|
456
|
+
}).where("reconciliation.collection_id", "=", input.claim.collectionId).where("reconciliation.run_token", "=", input.claim.runToken).where("reconciliation.target_epoch", "=", input.targetEpoch).where("reconciliation.state", "=", "leased").where("reconciliation.phase", "=", "scan").where("reconciliation.lease_token", "=", input.claim.leaseToken).where(liveLease(this.db)).where(this.statusOwnsRun(input.claim, input.targetEpoch));
|
|
457
|
+
query = input.previousCursor ? query.where("reconciliation.scan_cursor", "=", input.previousCursor) : query.where("reconciliation.scan_cursor", "is", null);
|
|
458
|
+
const result = await query.executeTakeFirst();
|
|
459
|
+
return Number(result.numUpdatedRows ?? 0) === 1;
|
|
460
|
+
}
|
|
461
|
+
async ownsRun(claim, targetEpoch) {
|
|
462
|
+
const result = await sql`
|
|
463
|
+
SELECT ${this.statusOwnsRun(claim, targetEpoch)} AS owned
|
|
464
|
+
`.execute(this.db);
|
|
465
|
+
return Boolean(result.rows[0]?.owned);
|
|
466
|
+
}
|
|
467
|
+
async restartRun(claim, previousEpoch) {
|
|
468
|
+
const now = timestampOffset(this.db, 0);
|
|
469
|
+
const interruptedRestart = sql`status = 'running'
|
|
470
|
+
AND cursor = ${claim.runToken}
|
|
471
|
+
AND change_epoch > ${previousEpoch}`;
|
|
472
|
+
return (await this.db.updateTable("_emdash_media_usage_index_status as status").set({
|
|
473
|
+
status: "running",
|
|
474
|
+
started_at: sql`CASE WHEN ${interruptedRestart} THEN started_at ELSE ${now} END`,
|
|
475
|
+
completed_at: null,
|
|
476
|
+
cursor: claim.runToken,
|
|
477
|
+
last_error_code: null,
|
|
478
|
+
change_epoch: sql`CASE WHEN ${interruptedRestart} THEN change_epoch ELSE change_epoch + 1 END`,
|
|
479
|
+
reconciliation_required: 1,
|
|
480
|
+
updated_at: now
|
|
481
|
+
}).where("status.adapter_id", "=", CONTENT_ADAPTER_ID).where("status.scope_type", "=", COLLECTION_SCOPE).where("status.collection_id", "=", claim.collectionId).where("status.scope_key", "=", claim.collectionSlug).where("status.capture_state", "=", "active").where("status.reconciliation_required", "=", 1).where((eb) => eb.or([eb("status.status", "!=", "running"), eb("status.cursor", "=", claim.runToken)])).where(this.liveClaimExists(claim)).returning("change_epoch").executeTakeFirst())?.change_epoch ?? null;
|
|
482
|
+
}
|
|
483
|
+
async restartScan(input) {
|
|
484
|
+
const result = await this.db.updateTable("_emdash_media_usage_reconciliations as reconciliation").set({
|
|
485
|
+
target_epoch: input.targetEpoch,
|
|
486
|
+
field_fingerprint: input.fieldFingerprint,
|
|
487
|
+
phase: "scan",
|
|
488
|
+
scan_cursor: null,
|
|
489
|
+
scan_upper_id: input.scanUpperId,
|
|
490
|
+
source_cursor: null,
|
|
491
|
+
source_upper_key: null,
|
|
492
|
+
attempt_count: 0,
|
|
493
|
+
next_attempt_at: timestampOffset(this.db, 0),
|
|
494
|
+
last_error_code: null,
|
|
495
|
+
updated_at: timestampOffset(this.db, 0)
|
|
496
|
+
}).where("reconciliation.collection_id", "=", input.claim.collectionId).where("reconciliation.run_token", "=", input.claim.runToken).where("reconciliation.target_epoch", "=", input.previousEpoch).where("reconciliation.state", "=", "leased").where("reconciliation.lease_token", "=", input.claim.leaseToken).where(liveLease(this.db)).where(this.statusOwnsRun(input.claim, input.targetEpoch)).executeTakeFirst();
|
|
497
|
+
return Number(result.numUpdatedRows ?? 0) === 1;
|
|
498
|
+
}
|
|
499
|
+
async findWorkBarrier(collectionId) {
|
|
500
|
+
if (!collectionId) throw new Error("Reconciliation work barrier requires a collection ID");
|
|
501
|
+
const failed = await this.db.selectFrom("_emdash_media_usage_work").select("last_error_code").where("collection_id", "=", collectionId).where("state", "=", "failed").orderBy("content_id").limit(1).executeTakeFirst();
|
|
502
|
+
if (failed) return {
|
|
503
|
+
state: "failed",
|
|
504
|
+
errorCode: failed.last_error_code ?? "MEDIA_USAGE_PROCESSING_FAILED"
|
|
505
|
+
};
|
|
506
|
+
return await this.db.selectFrom("_emdash_media_usage_work").select("content_id").where("collection_id", "=", collectionId).limit(1).executeTakeFirst() ? { state: "pending" } : { state: "empty" };
|
|
507
|
+
}
|
|
508
|
+
async findSourceUpperKey(claim, targetEpoch) {
|
|
509
|
+
return (await this.db.selectFrom("_emdash_media_usage_sources as source").select("source.source_key").where("source.source_type", "=", "content").where("source.collection_id", "=", claim.collectionId).where("source.identity_version", "=", 1).where(this.liveClaimExists(claim)).where(this.statusOwnsRun(claim, targetEpoch)).orderBy("source.source_key", "desc").limit(1).executeTakeFirst())?.source_key ?? null;
|
|
510
|
+
}
|
|
511
|
+
async transitionToSources(input) {
|
|
512
|
+
const result = await this.db.updateTable("_emdash_media_usage_reconciliations as reconciliation").set({
|
|
513
|
+
phase: "sources",
|
|
514
|
+
source_cursor: null,
|
|
515
|
+
source_upper_key: input.sourceUpperKey,
|
|
516
|
+
attempt_count: 0,
|
|
517
|
+
last_error_code: null,
|
|
518
|
+
updated_at: timestampOffset(this.db, 0)
|
|
519
|
+
}).where("reconciliation.collection_id", "=", input.claim.collectionId).where("reconciliation.run_token", "=", input.claim.runToken).where("reconciliation.target_epoch", "=", input.targetEpoch).where("reconciliation.field_fingerprint", "=", input.fieldFingerprint).where("reconciliation.state", "=", "leased").where("reconciliation.phase", "=", "scan").where("reconciliation.lease_token", "=", input.claim.leaseToken).where(liveLease(this.db)).where(this.statusOwnsRun(input.claim, input.targetEpoch)).where((eb) => eb.not(eb.exists(eb.selectFrom("_emdash_media_usage_work as work").select("work.content_id").where("work.collection_id", "=", input.claim.collectionId)))).executeTakeFirst();
|
|
520
|
+
return Number(result.numUpdatedRows ?? 0) === 1;
|
|
521
|
+
}
|
|
522
|
+
async findSourcePage(reconciliation, limit) {
|
|
523
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 50) throw new Error("Reconciliation source page limit must be from 1 to 50");
|
|
524
|
+
if (!reconciliation.leaseToken || reconciliation.targetEpoch === null) return [];
|
|
525
|
+
let query = this.db.selectFrom("_emdash_media_usage_sources as source").select([
|
|
526
|
+
"source.source_key",
|
|
527
|
+
"source.content_id",
|
|
528
|
+
"source.source_variant"
|
|
529
|
+
]).where("source.source_type", "=", "content").where("source.collection_id", "=", reconciliation.collectionId).where("source.identity_version", "=", 1).where(this.liveClaimExists(reconciliation)).where(this.statusOwnsRun(reconciliation, reconciliation.targetEpoch));
|
|
530
|
+
if (reconciliation.sourceCursor) query = query.where("source.source_key", ">", reconciliation.sourceCursor);
|
|
531
|
+
if (reconciliation.sourceUpperKey) query = query.where("source.source_key", "<=", reconciliation.sourceUpperKey);
|
|
532
|
+
else query = query.where(sql`1 = 0`);
|
|
533
|
+
return (await query.orderBy("source.source_key").limit(limit).execute()).map((row) => ({
|
|
534
|
+
sourceKey: row.source_key,
|
|
535
|
+
contentId: row.content_id,
|
|
536
|
+
sourceVariant: row.source_variant
|
|
537
|
+
}));
|
|
538
|
+
}
|
|
539
|
+
async findMissingContentIds(collectionSlug, contentIds) {
|
|
540
|
+
const unique = [...new Set(contentIds)];
|
|
541
|
+
if (unique.length === 0) return [];
|
|
542
|
+
if (unique.length > 50 || unique.some((contentId) => !contentId)) throw new Error("Reconciliation source page has invalid content identity");
|
|
543
|
+
const tableName = contentTableName(collectionSlug);
|
|
544
|
+
const existing = await sql`
|
|
545
|
+
SELECT id FROM ${sql.ref(tableName)} WHERE id IN (${sql.join(unique)})
|
|
546
|
+
`.execute(this.db);
|
|
547
|
+
const present = new Set(existing.rows.map((row) => row.id));
|
|
548
|
+
return unique.filter((contentId) => !present.has(contentId));
|
|
549
|
+
}
|
|
550
|
+
async checkpointSources(input) {
|
|
551
|
+
let query = this.db.updateTable("_emdash_media_usage_reconciliations as reconciliation").set({
|
|
552
|
+
source_cursor: input.nextCursor,
|
|
553
|
+
attempt_count: 0,
|
|
554
|
+
last_error_code: null,
|
|
555
|
+
updated_at: timestampOffset(this.db, 0)
|
|
556
|
+
}).where("reconciliation.collection_id", "=", input.claim.collectionId).where("reconciliation.run_token", "=", input.claim.runToken).where("reconciliation.target_epoch", "=", input.targetEpoch).where("reconciliation.state", "=", "leased").where("reconciliation.phase", "=", "sources").where("reconciliation.lease_token", "=", input.claim.leaseToken).where(liveLease(this.db)).where(this.statusOwnsRun(input.claim, input.targetEpoch));
|
|
557
|
+
query = input.previousCursor ? query.where("reconciliation.source_cursor", "=", input.previousCursor) : query.where("reconciliation.source_cursor", "is", null);
|
|
558
|
+
const result = await query.executeTakeFirst();
|
|
559
|
+
return Number(result.numUpdatedRows ?? 0) === 1;
|
|
560
|
+
}
|
|
561
|
+
async finishFailedCoverage(collectionId, runToken) {
|
|
562
|
+
assertIdentity({
|
|
563
|
+
collectionId,
|
|
564
|
+
runToken
|
|
565
|
+
});
|
|
566
|
+
const now = timestampOffset(this.db, 0);
|
|
567
|
+
const result = await this.db.updateTable("_emdash_media_usage_index_status as status").set({
|
|
568
|
+
status: sql`CASE WHEN EXISTS (
|
|
569
|
+
SELECT 1 FROM _emdash_media_usage_sources AS source
|
|
570
|
+
WHERE source.source_type = 'content'
|
|
571
|
+
AND source.collection_id = ${collectionId}
|
|
572
|
+
AND source.identity_version = 1
|
|
573
|
+
LIMIT 1
|
|
574
|
+
) THEN 'partial' ELSE 'failed' END`,
|
|
575
|
+
completed_at: null,
|
|
576
|
+
cursor: null,
|
|
577
|
+
last_error_code: sql`CASE WHEN (
|
|
578
|
+
SELECT reconciliation.last_error_code
|
|
579
|
+
FROM _emdash_media_usage_reconciliations AS reconciliation
|
|
580
|
+
WHERE reconciliation.collection_id = ${collectionId}
|
|
581
|
+
AND reconciliation.run_token = ${runToken}
|
|
582
|
+
) = 'MEDIA_USAGE_RECONCILIATION_ENTRY_FAILED'
|
|
583
|
+
THEN COALESCE(
|
|
584
|
+
(SELECT work.last_error_code
|
|
585
|
+
FROM _emdash_media_usage_work AS work
|
|
586
|
+
WHERE work.collection_id = ${collectionId} AND work.state = 'failed'
|
|
587
|
+
ORDER BY work.content_id LIMIT 1),
|
|
588
|
+
'MEDIA_USAGE_RECONCILIATION_ENTRY_FAILED'
|
|
589
|
+
)
|
|
590
|
+
ELSE (
|
|
591
|
+
SELECT reconciliation.last_error_code
|
|
592
|
+
FROM _emdash_media_usage_reconciliations AS reconciliation
|
|
593
|
+
WHERE reconciliation.collection_id = ${collectionId}
|
|
594
|
+
AND reconciliation.run_token = ${runToken}
|
|
595
|
+
) END`,
|
|
596
|
+
reconciliation_required: 1,
|
|
597
|
+
updated_at: now
|
|
598
|
+
}).where("status.adapter_id", "=", CONTENT_ADAPTER_ID).where("status.scope_type", "=", COLLECTION_SCOPE).where("status.collection_id", "=", collectionId).where("status.status", "=", "running").where("status.cursor", "=", runToken).where((eb) => eb.exists(eb.selectFrom("_emdash_media_usage_reconciliations as reconciliation").select("reconciliation.collection_id").where("reconciliation.collection_id", "=", collectionId).where("reconciliation.run_token", "=", runToken).where("reconciliation.state", "=", "failed"))).executeTakeFirst();
|
|
599
|
+
return Number(result.numUpdatedRows ?? 0) === 1;
|
|
600
|
+
}
|
|
601
|
+
async finalizeCoverage(input) {
|
|
602
|
+
const now = timestampOffset(this.db, 0);
|
|
603
|
+
const result = await this.db.updateTable("_emdash_media_usage_index_status as status").set({
|
|
604
|
+
status: "complete",
|
|
605
|
+
schema_version: input.schemaVersion,
|
|
606
|
+
completed_at: now,
|
|
607
|
+
cursor: null,
|
|
608
|
+
last_error_code: null,
|
|
609
|
+
reconciliation_required: 0,
|
|
610
|
+
updated_at: now
|
|
611
|
+
}).where("status.adapter_id", "=", CONTENT_ADAPTER_ID).where("status.scope_type", "=", COLLECTION_SCOPE).where("status.collection_id", "=", input.claim.collectionId).where("status.scope_key", "=", input.claim.collectionSlug).where("status.capture_state", "=", "active").where("status.reconciliation_required", "=", 1).where("status.status", "=", "running").where("status.cursor", "=", input.claim.runToken).where("status.change_epoch", "=", input.targetEpoch).where((eb) => eb.not(eb.exists(eb.selectFrom("_emdash_media_usage_work as work").select("work.content_id").where("work.collection_id", "=", input.claim.collectionId)))).where((eb) => eb.exists(eb.selectFrom("_emdash_media_usage_reconciliations as reconciliation").innerJoin("_emdash_collections as collection", (join) => join.onRef("collection.id", "=", "reconciliation.collection_id").onRef("collection.slug", "=", "reconciliation.collection_slug")).select("reconciliation.collection_id").where("reconciliation.collection_id", "=", input.claim.collectionId).where("reconciliation.run_token", "=", input.claim.runToken).where("reconciliation.target_epoch", "=", input.targetEpoch).where("reconciliation.field_fingerprint", "=", input.fieldFingerprint).where("reconciliation.state", "=", "leased").where("reconciliation.phase", "=", "sources").where("reconciliation.lease_token", "=", input.claim.leaseToken).where(liveLease(this.db, "reconciliation.lease_expires_at")).where((inner) => inner.not(inner.exists(inner.selectFrom("_emdash_media_usage_collection_deletions as deletion").select("deletion.collection_id").where("deletion.collection_id", "=", input.claim.collectionId)))))).where((eb) => eb.exists(eb.selectFrom("_emdash_media_usage_activation as activation").select("activation.task_key").where("activation.task_key", "=", ACTIVATION_KEY).where("activation.state", "=", "active"))).executeTakeFirst();
|
|
612
|
+
return Number(result.numUpdatedRows ?? 0) === 1;
|
|
613
|
+
}
|
|
614
|
+
async deleteFinalized(claim) {
|
|
615
|
+
const result = await this.db.deleteFrom("_emdash_media_usage_reconciliations as reconciliation").where("reconciliation.collection_id", "=", claim.collectionId).where("reconciliation.run_token", "=", claim.runToken).where("reconciliation.state", "=", "leased").where("reconciliation.lease_token", "=", claim.leaseToken).where((eb) => eb.exists(eb.selectFrom("_emdash_media_usage_index_status as status").select("status.collection_id").where("status.collection_id", "=", claim.collectionId).where("status.scope_key", "=", claim.collectionSlug).where("status.status", "=", "complete").where("status.reconciliation_required", "=", 0))).executeTakeFirst();
|
|
616
|
+
return Number(result.numDeletedRows ?? 0) === 1;
|
|
617
|
+
}
|
|
618
|
+
async deleteOneObsolete() {
|
|
619
|
+
return (await sql`
|
|
620
|
+
DELETE FROM _emdash_media_usage_reconciliations
|
|
621
|
+
WHERE (collection_id, run_token) IN (
|
|
622
|
+
SELECT reconciliation.collection_id, reconciliation.run_token
|
|
623
|
+
FROM _emdash_media_usage_reconciliations AS reconciliation
|
|
624
|
+
INNER JOIN _emdash_media_usage_index_status AS status
|
|
625
|
+
ON status.collection_id = reconciliation.collection_id
|
|
626
|
+
AND status.scope_key = reconciliation.collection_slug
|
|
627
|
+
WHERE status.adapter_id = ${CONTENT_ADAPTER_ID}
|
|
628
|
+
AND status.scope_type = ${COLLECTION_SCOPE}
|
|
629
|
+
AND status.reconciliation_required = 0
|
|
630
|
+
ORDER BY reconciliation.updated_at, reconciliation.collection_id
|
|
631
|
+
LIMIT 1
|
|
632
|
+
)
|
|
633
|
+
RETURNING collection_id
|
|
634
|
+
`.execute(this.db)).rows.length === 1;
|
|
635
|
+
}
|
|
636
|
+
liveClaimExists(claim) {
|
|
637
|
+
return this.liveClaimExistsSql(claim);
|
|
638
|
+
}
|
|
639
|
+
liveClaimExistsSql(claim) {
|
|
640
|
+
return sql`EXISTS (
|
|
641
|
+
SELECT 1
|
|
642
|
+
FROM _emdash_media_usage_reconciliations AS reconciliation
|
|
643
|
+
INNER JOIN _emdash_collections AS collection
|
|
644
|
+
ON collection.id = reconciliation.collection_id
|
|
645
|
+
AND collection.slug = reconciliation.collection_slug
|
|
646
|
+
WHERE reconciliation.collection_id = ${claim.collectionId}
|
|
647
|
+
AND reconciliation.collection_slug = ${claim.collectionSlug}
|
|
648
|
+
AND reconciliation.run_token = ${claim.runToken}
|
|
649
|
+
AND reconciliation.state = 'leased'
|
|
650
|
+
AND reconciliation.lease_token = ${claim.leaseToken}
|
|
651
|
+
AND ${liveLease(this.db, "reconciliation.lease_expires_at")}
|
|
652
|
+
AND EXISTS (
|
|
653
|
+
SELECT 1 FROM _emdash_media_usage_activation AS activation
|
|
654
|
+
WHERE activation.task_key = ${ACTIVATION_KEY}
|
|
655
|
+
AND activation.state = 'active'
|
|
656
|
+
)
|
|
657
|
+
AND NOT EXISTS (
|
|
658
|
+
SELECT 1 FROM _emdash_media_usage_collection_deletions AS deletion
|
|
659
|
+
WHERE deletion.collection_id = reconciliation.collection_id
|
|
660
|
+
)
|
|
661
|
+
)`;
|
|
662
|
+
}
|
|
663
|
+
statusOwnsRun(claim, targetEpoch) {
|
|
664
|
+
return sql`EXISTS (
|
|
665
|
+
SELECT 1 FROM _emdash_media_usage_index_status AS status
|
|
666
|
+
WHERE status.adapter_id = ${CONTENT_ADAPTER_ID}
|
|
667
|
+
AND status.scope_type = ${COLLECTION_SCOPE}
|
|
668
|
+
AND status.collection_id = ${claim.collectionId}
|
|
669
|
+
AND status.scope_key = ${claim.collectionSlug}
|
|
670
|
+
AND status.capture_state = 'active'
|
|
671
|
+
AND status.reconciliation_required = 1
|
|
672
|
+
AND status.status = 'running'
|
|
673
|
+
AND status.cursor = ${claim.runToken}
|
|
674
|
+
AND status.change_epoch = ${targetEpoch}
|
|
675
|
+
)`;
|
|
676
|
+
}
|
|
677
|
+
async seedNextCandidate() {
|
|
678
|
+
const runToken = ulid();
|
|
679
|
+
const now = timestampOffset(this.db, 0);
|
|
680
|
+
return (await sql`
|
|
681
|
+
INSERT INTO _emdash_media_usage_reconciliations (
|
|
682
|
+
collection_id,
|
|
683
|
+
collection_slug,
|
|
684
|
+
run_token,
|
|
685
|
+
next_attempt_at,
|
|
686
|
+
updated_at
|
|
687
|
+
)
|
|
688
|
+
SELECT status.collection_id, status.scope_key, ${runToken}, ${now}, ${now}
|
|
689
|
+
FROM _emdash_media_usage_index_status AS status
|
|
690
|
+
INNER JOIN _emdash_collections AS collection
|
|
691
|
+
ON collection.id = status.collection_id
|
|
692
|
+
AND collection.slug = status.scope_key
|
|
693
|
+
WHERE status.adapter_id = ${CONTENT_ADAPTER_ID}
|
|
694
|
+
AND status.scope_type = ${COLLECTION_SCOPE}
|
|
695
|
+
AND status.capture_state = 'active'
|
|
696
|
+
AND status.reconciliation_required = 1
|
|
697
|
+
AND status.collection_id IS NOT NULL
|
|
698
|
+
AND EXISTS (
|
|
699
|
+
SELECT 1 FROM _emdash_media_usage_activation AS activation
|
|
700
|
+
WHERE activation.task_key = ${ACTIVATION_KEY}
|
|
701
|
+
AND activation.state = 'active'
|
|
702
|
+
)
|
|
703
|
+
AND NOT EXISTS (
|
|
704
|
+
SELECT 1 FROM _emdash_media_usage_reconciliations AS existing
|
|
705
|
+
WHERE existing.collection_id = status.collection_id
|
|
706
|
+
)
|
|
707
|
+
AND NOT EXISTS (
|
|
708
|
+
SELECT 1 FROM _emdash_media_usage_collection_deletions AS deletion
|
|
709
|
+
WHERE deletion.collection_id = status.collection_id
|
|
710
|
+
)
|
|
711
|
+
ORDER BY status.collection_id
|
|
712
|
+
LIMIT 1
|
|
713
|
+
ON CONFLICT (collection_id) DO NOTHING
|
|
714
|
+
RETURNING collection_id
|
|
715
|
+
`.execute(this.db)).rows.length === 1;
|
|
716
|
+
}
|
|
717
|
+
async findDue(limit) {
|
|
718
|
+
assertLimit(limit);
|
|
719
|
+
const nextAttemptIsDue = timestampIsDue(this.db, "next_attempt_at");
|
|
720
|
+
return (await sql`
|
|
721
|
+
WITH pending_candidates AS (
|
|
722
|
+
SELECT * FROM _emdash_media_usage_reconciliations
|
|
723
|
+
WHERE state = 'pending' AND ${nextAttemptIsDue}
|
|
724
|
+
ORDER BY next_attempt_at, updated_at, collection_id
|
|
725
|
+
LIMIT ${limit}
|
|
726
|
+
), retry_candidates AS (
|
|
727
|
+
SELECT * FROM _emdash_media_usage_reconciliations
|
|
728
|
+
WHERE state = 'retry' AND ${nextAttemptIsDue}
|
|
729
|
+
ORDER BY next_attempt_at, updated_at, collection_id
|
|
730
|
+
LIMIT ${limit}
|
|
731
|
+
), leased_candidates AS (
|
|
732
|
+
SELECT * FROM _emdash_media_usage_reconciliations
|
|
733
|
+
WHERE state = 'leased' AND ${timestampIsDue(this.db, "lease_expires_at")}
|
|
734
|
+
ORDER BY lease_expires_at, updated_at, collection_id
|
|
735
|
+
LIMIT ${limit}
|
|
736
|
+
), candidates AS (
|
|
737
|
+
SELECT * FROM pending_candidates
|
|
738
|
+
UNION ALL SELECT * FROM retry_candidates
|
|
739
|
+
UNION ALL SELECT * FROM leased_candidates
|
|
740
|
+
)
|
|
741
|
+
SELECT * FROM candidates
|
|
742
|
+
ORDER BY CASE WHEN state = 'leased' THEN lease_expires_at ELSE next_attempt_at END,
|
|
743
|
+
updated_at,
|
|
744
|
+
collection_id
|
|
745
|
+
LIMIT ${limit}
|
|
746
|
+
`.execute(this.db)).rows.map(rowToRecord);
|
|
747
|
+
}
|
|
748
|
+
async findFailed(limit) {
|
|
749
|
+
assertLimit(limit);
|
|
750
|
+
return (await this.db.selectFrom("_emdash_media_usage_reconciliations as reconciliation").innerJoin("_emdash_media_usage_index_status as status", (join) => join.onRef("status.collection_id", "=", "reconciliation.collection_id").onRef("status.scope_key", "=", "reconciliation.collection_slug")).selectAll("reconciliation").where("reconciliation.state", "=", "failed").where("status.adapter_id", "=", CONTENT_ADAPTER_ID).where("status.scope_type", "=", COLLECTION_SCOPE).where((eb) => eb.or([
|
|
751
|
+
eb("status.reconciliation_required", "=", 0),
|
|
752
|
+
eb.and([eb("status.status", "=", "running"), eb("status.cursor", "=", eb.ref("reconciliation.run_token"))]),
|
|
753
|
+
eb.and([eb("status.cursor", "is", null), eb("status.change_epoch", ">", eb.ref("reconciliation.target_epoch"))])
|
|
754
|
+
])).orderBy("reconciliation.updated_at").orderBy("reconciliation.collection_id").limit(limit).execute()).map(rowToRecord);
|
|
755
|
+
}
|
|
756
|
+
async claim(input) {
|
|
757
|
+
assertIdentity(input);
|
|
758
|
+
assertDuration(input.leaseDurationSeconds, "lease duration");
|
|
759
|
+
const leaseToken = ulid();
|
|
760
|
+
const row = await this.db.updateTable("_emdash_media_usage_reconciliations as reconciliation").set({
|
|
761
|
+
state: "leased",
|
|
762
|
+
lease_token: leaseToken,
|
|
763
|
+
lease_expires_at: timestampOffset(this.db, input.leaseDurationSeconds),
|
|
764
|
+
updated_at: timestampOffset(this.db, 0)
|
|
765
|
+
}).where("reconciliation.collection_id", "=", input.collectionId).where("reconciliation.run_token", "=", input.runToken).where((eb) => eb.or([eb.and([eb("reconciliation.state", "in", ["pending", "retry"]), timestampIsDue(this.db, "reconciliation.next_attempt_at")]), eb.and([eb("reconciliation.state", "=", "leased"), timestampIsDue(this.db, "reconciliation.lease_expires_at")])])).where((eb) => eb.exists(eb.selectFrom("_emdash_media_usage_activation as activation").select("activation.task_key").where("activation.task_key", "=", ACTIVATION_KEY).where("activation.state", "=", "active"))).where((eb) => eb.exists(eb.selectFrom("_emdash_media_usage_index_status as status").innerJoin("_emdash_collections as collection", (join) => join.onRef("collection.id", "=", "status.collection_id").onRef("collection.slug", "=", "status.scope_key")).select("status.collection_id").whereRef("status.collection_id", "=", "reconciliation.collection_id").whereRef("status.scope_key", "=", "reconciliation.collection_slug").where("status.adapter_id", "=", CONTENT_ADAPTER_ID).where("status.scope_type", "=", COLLECTION_SCOPE).where("status.capture_state", "=", "active").where("status.reconciliation_required", "=", 1))).where((eb) => eb.not(eb.exists(eb.selectFrom("_emdash_media_usage_collection_deletions as deletion").select("deletion.collection_id").whereRef("deletion.collection_id", "=", "reconciliation.collection_id")))).returningAll().executeTakeFirst();
|
|
766
|
+
return row ? {
|
|
767
|
+
...rowToRecord(row),
|
|
768
|
+
leaseToken
|
|
769
|
+
} : null;
|
|
770
|
+
}
|
|
771
|
+
async release(input) {
|
|
772
|
+
assertLeaseIdentity(input);
|
|
773
|
+
assertDuration(input.delaySeconds, "release delay", true);
|
|
774
|
+
const result = await this.db.updateTable("_emdash_media_usage_reconciliations").set({
|
|
775
|
+
state: "pending",
|
|
776
|
+
next_attempt_at: timestampOffset(this.db, input.delaySeconds),
|
|
777
|
+
lease_token: null,
|
|
778
|
+
lease_expires_at: null,
|
|
779
|
+
updated_at: timestampOffset(this.db, 0)
|
|
780
|
+
}).where("collection_id", "=", input.collectionId).where("run_token", "=", input.runToken).where("state", "=", "leased").where("lease_token", "=", input.leaseToken).where(liveLease(this.db)).executeTakeFirst();
|
|
781
|
+
return Number(result.numUpdatedRows ?? 0) === 1;
|
|
782
|
+
}
|
|
783
|
+
async recordFailure(input) {
|
|
784
|
+
assertLeaseIdentity(input);
|
|
785
|
+
if (!STABLE_ERROR_CODE_PATTERN.test(input.errorCode)) throw new Error("Reconciliation failure requires a stable error code");
|
|
786
|
+
assertDuration(input.retryDelaySeconds, "retry delay", true);
|
|
787
|
+
const result = await this.db.updateTable("_emdash_media_usage_reconciliations").set({
|
|
788
|
+
state: input.terminal ? "failed" : sql`CASE WHEN attempt_count >= 4 THEN 'failed' ELSE 'retry' END`,
|
|
789
|
+
...input.terminal ? { target_epoch: sql`COALESCE(
|
|
790
|
+
target_epoch,
|
|
791
|
+
(SELECT status.change_epoch
|
|
792
|
+
FROM _emdash_media_usage_index_status AS status
|
|
793
|
+
WHERE status.adapter_id = ${CONTENT_ADAPTER_ID}
|
|
794
|
+
AND status.scope_type = ${COLLECTION_SCOPE}
|
|
795
|
+
AND status.collection_id = ${input.collectionId}
|
|
796
|
+
AND status.status = 'running'
|
|
797
|
+
AND status.cursor = ${input.runToken})
|
|
798
|
+
)` } : {},
|
|
799
|
+
attempt_count: sql`attempt_count + 1`,
|
|
800
|
+
next_attempt_at: timestampOffset(this.db, input.retryDelaySeconds),
|
|
801
|
+
lease_token: null,
|
|
802
|
+
lease_expires_at: null,
|
|
803
|
+
last_error_code: input.errorCode,
|
|
804
|
+
updated_at: timestampOffset(this.db, 0)
|
|
805
|
+
}).where("collection_id", "=", input.collectionId).where("run_token", "=", input.runToken).where("state", "=", "leased").where("lease_token", "=", input.leaseToken).where(liveLease(this.db)).executeTakeFirst();
|
|
806
|
+
return Number(result.numUpdatedRows ?? 0) === 1;
|
|
807
|
+
}
|
|
808
|
+
async recordEntryFailure(claim) {
|
|
809
|
+
const result = await this.db.updateTable("_emdash_media_usage_reconciliations as reconciliation").set({
|
|
810
|
+
state: "failed",
|
|
811
|
+
attempt_count: sql`attempt_count + 1`,
|
|
812
|
+
next_attempt_at: timestampOffset(this.db, 0),
|
|
813
|
+
lease_token: null,
|
|
814
|
+
lease_expires_at: null,
|
|
815
|
+
last_error_code: "MEDIA_USAGE_RECONCILIATION_ENTRY_FAILED",
|
|
816
|
+
updated_at: timestampOffset(this.db, 0)
|
|
817
|
+
}).where("reconciliation.collection_id", "=", claim.collectionId).where("reconciliation.run_token", "=", claim.runToken).where("reconciliation.state", "=", "leased").where("reconciliation.lease_token", "=", claim.leaseToken).where(liveLease(this.db)).where((eb) => eb.exists(eb.selectFrom("_emdash_media_usage_work as work").select("work.content_id").where("work.collection_id", "=", claim.collectionId).where("work.state", "=", "failed"))).executeTakeFirst();
|
|
818
|
+
return Number(result.numUpdatedRows ?? 0) === 1;
|
|
819
|
+
}
|
|
820
|
+
async resetFailedForNewEpoch(observed) {
|
|
821
|
+
const collectionId = "collection_id" in observed ? observed.collection_id : observed.collectionId;
|
|
822
|
+
const runToken = "run_token" in observed ? observed.run_token : observed.runToken;
|
|
823
|
+
const targetEpoch = "target_epoch" in observed ? observed.target_epoch : observed.targetEpoch;
|
|
824
|
+
if (targetEpoch === null) return false;
|
|
825
|
+
const now = timestampOffset(this.db, 0);
|
|
826
|
+
const result = await this.db.updateTable("_emdash_media_usage_reconciliations as reconciliation").set({
|
|
827
|
+
state: "pending",
|
|
828
|
+
phase: "scan",
|
|
829
|
+
target_epoch: null,
|
|
830
|
+
field_fingerprint: null,
|
|
831
|
+
scan_cursor: null,
|
|
832
|
+
scan_upper_id: null,
|
|
833
|
+
source_cursor: null,
|
|
834
|
+
source_upper_key: null,
|
|
835
|
+
attempt_count: 0,
|
|
836
|
+
next_attempt_at: now,
|
|
837
|
+
lease_token: null,
|
|
838
|
+
lease_expires_at: null,
|
|
839
|
+
last_error_code: null,
|
|
840
|
+
updated_at: now
|
|
841
|
+
}).where("reconciliation.collection_id", "=", collectionId).where("reconciliation.run_token", "=", runToken).where("reconciliation.state", "=", "failed").where("reconciliation.target_epoch", "=", targetEpoch).where((eb) => eb.exists(eb.selectFrom("_emdash_media_usage_index_status as status").select("status.collection_id").whereRef("status.collection_id", "=", "reconciliation.collection_id").whereRef("status.scope_key", "=", "reconciliation.collection_slug").where("status.adapter_id", "=", CONTENT_ADAPTER_ID).where("status.scope_type", "=", COLLECTION_SCOPE).where("status.capture_state", "=", "active").where("status.reconciliation_required", "=", 1).where("status.cursor", "is", null).where("status.change_epoch", ">", targetEpoch))).executeTakeFirst();
|
|
842
|
+
return Number(result.numUpdatedRows ?? 0) === 1;
|
|
843
|
+
}
|
|
844
|
+
};
|
|
845
|
+
function rowToRecord(row) {
|
|
846
|
+
if (!isState(row.state) || !isPhase(row.phase) || !Number.isSafeInteger(row.attempt_count)) throw new Error("Invalid media usage reconciliation lifecycle");
|
|
847
|
+
return {
|
|
848
|
+
collectionId: row.collection_id,
|
|
849
|
+
collectionSlug: row.collection_slug,
|
|
850
|
+
runToken: row.run_token,
|
|
851
|
+
targetEpoch: row.target_epoch,
|
|
852
|
+
fieldFingerprint: row.field_fingerprint,
|
|
853
|
+
state: row.state,
|
|
854
|
+
phase: row.phase,
|
|
855
|
+
scanCursor: row.scan_cursor,
|
|
856
|
+
scanUpperId: row.scan_upper_id,
|
|
857
|
+
sourceCursor: row.source_cursor,
|
|
858
|
+
sourceUpperKey: row.source_upper_key,
|
|
859
|
+
attemptCount: row.attempt_count,
|
|
860
|
+
nextAttemptAt: row.next_attempt_at,
|
|
861
|
+
leaseToken: row.lease_token,
|
|
862
|
+
leaseExpiresAt: row.lease_expires_at,
|
|
863
|
+
lastErrorCode: row.last_error_code,
|
|
864
|
+
createdAt: row.created_at,
|
|
865
|
+
updatedAt: row.updated_at
|
|
866
|
+
};
|
|
867
|
+
}
|
|
868
|
+
function isState(state) {
|
|
869
|
+
return state === "pending" || state === "retry" || state === "leased" || state === "failed";
|
|
870
|
+
}
|
|
871
|
+
function isPhase(phase) {
|
|
872
|
+
return phase === "scan" || phase === "sources";
|
|
873
|
+
}
|
|
874
|
+
function assertLimit(limit) {
|
|
875
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > MAX_CANDIDATES) throw new Error("Reconciliation candidate limit must be from 1 to 100");
|
|
876
|
+
}
|
|
877
|
+
function assertIdentity(input) {
|
|
878
|
+
if (!input.collectionId || !input.runToken) throw new Error("Reconciliation requires an exact collection and run token");
|
|
879
|
+
}
|
|
880
|
+
function assertLeaseIdentity(input) {
|
|
881
|
+
assertIdentity(input);
|
|
882
|
+
if (!input.leaseToken) throw new Error("Reconciliation requires a lease token");
|
|
883
|
+
}
|
|
884
|
+
function assertDuration(value, label, allowZero = false) {
|
|
885
|
+
if (!Number.isSafeInteger(value) || value < (allowZero ? 0 : 1) || value > MAX_PORTABLE_DURATION_SECONDS) throw new Error(`Reconciliation ${label} is outside the portable range`);
|
|
886
|
+
}
|
|
887
|
+
function liveLease(db, column = "lease_expires_at") {
|
|
888
|
+
const expiry = sql.ref(column);
|
|
889
|
+
return isPostgres(db) ? sql`${expiry} > to_char(statement_timestamp() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')` : sql`${expiry} > strftime('%Y-%m-%dT%H:%M:%fZ', 'now')`;
|
|
890
|
+
}
|
|
891
|
+
function timestampIsDue(db, column) {
|
|
892
|
+
const value = sql.ref(column);
|
|
893
|
+
return isPostgres(db) ? sql`${value} <= to_char(statement_timestamp() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')` : sql`${value} <= strftime('%Y-%m-%dT%H:%M:%fZ', 'now')`;
|
|
894
|
+
}
|
|
895
|
+
function timestampOffset(db, offsetSeconds) {
|
|
896
|
+
if (isPostgres(db)) return sql`to_char(
|
|
897
|
+
(clock_timestamp() AT TIME ZONE 'UTC') + (${offsetSeconds} * INTERVAL '1 second'),
|
|
898
|
+
'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'
|
|
899
|
+
)`;
|
|
900
|
+
return sql`strftime(
|
|
901
|
+
'%Y-%m-%dT%H:%M:%fZ',
|
|
902
|
+
'now',
|
|
903
|
+
${`${offsetSeconds >= 0 ? "+" : ""}${offsetSeconds} seconds`}
|
|
904
|
+
)`;
|
|
905
|
+
}
|
|
906
|
+
function contentTableName(collectionSlug) {
|
|
907
|
+
validateIdentifier(collectionSlug, "collection slug");
|
|
908
|
+
const tableName = `ec_${collectionSlug}`;
|
|
909
|
+
validateIdentifier(tableName, "content table");
|
|
910
|
+
return tableName;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
//#endregion
|
|
914
|
+
//#region src/media/usage/reconciliation-processor.ts
|
|
915
|
+
const MEDIA_USAGE_RECONCILIATION_LIMITS = Object.freeze({
|
|
916
|
+
candidatesPerTick: 4,
|
|
917
|
+
pageSize: 50,
|
|
918
|
+
leaseDurationSeconds: 60,
|
|
919
|
+
maxAttempts: 5,
|
|
920
|
+
retryBaseSeconds: 30,
|
|
921
|
+
retryMaxSeconds: 900,
|
|
922
|
+
retryJitterRatio: .25,
|
|
923
|
+
maxQueriesPerTick: 20
|
|
924
|
+
});
|
|
925
|
+
async function processDueMediaUsageReconciliation(db) {
|
|
926
|
+
if ((await db.selectFrom("_emdash_media_usage_activation").select("state").where("task_key", "=", "incremental_capture").executeTakeFirst())?.state !== "active") return "inactive";
|
|
927
|
+
const reconciliation = new MediaUsageReconciliationRepository(db);
|
|
928
|
+
if (await reconciliation.deleteOneObsolete()) return "completed";
|
|
929
|
+
const [failed] = await reconciliation.findFailed(1);
|
|
930
|
+
if (failed) {
|
|
931
|
+
if (await reconciliation.finishFailedCoverage(failed.collectionId, failed.runToken)) return "failed";
|
|
932
|
+
if (await reconciliation.resetFailedForNewEpoch(failed)) return "advanced";
|
|
933
|
+
}
|
|
934
|
+
await reconciliation.seedNextCandidate();
|
|
935
|
+
const candidates = await reconciliation.findDue(MEDIA_USAGE_RECONCILIATION_LIMITS.candidatesPerTick);
|
|
936
|
+
let claim = null;
|
|
937
|
+
for (const candidate of candidates) {
|
|
938
|
+
claim = await reconciliation.claim({
|
|
939
|
+
collectionId: candidate.collectionId,
|
|
940
|
+
runToken: candidate.runToken,
|
|
941
|
+
leaseDurationSeconds: MEDIA_USAGE_RECONCILIATION_LIMITS.leaseDurationSeconds
|
|
942
|
+
});
|
|
943
|
+
if (claim) break;
|
|
944
|
+
}
|
|
945
|
+
if (!claim) return candidates.length === 0 ? "not_due" : "claim_lost";
|
|
946
|
+
try {
|
|
947
|
+
return await processClaimedReconciliation(db, claim);
|
|
948
|
+
} catch (error) {
|
|
949
|
+
const terminal = claim.attemptCount + 1 >= MEDIA_USAGE_RECONCILIATION_LIMITS.maxAttempts;
|
|
950
|
+
if (!await reconciliation.recordFailure({
|
|
951
|
+
collectionId: claim.collectionId,
|
|
952
|
+
runToken: claim.runToken,
|
|
953
|
+
leaseToken: claim.leaseToken,
|
|
954
|
+
errorCode: "MEDIA_USAGE_RECONCILIATION_FAILED",
|
|
955
|
+
retryDelaySeconds: retryDelaySeconds$1(claim.attemptCount),
|
|
956
|
+
terminal
|
|
957
|
+
})) return "claim_lost";
|
|
958
|
+
if (terminal) await reconciliation.finishFailedCoverage(claim.collectionId, claim.runToken);
|
|
959
|
+
console.error("[media-usage:reconciliation] Processing failed:", error);
|
|
960
|
+
return terminal ? "failed" : "retry";
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
async function processClaimedMediaUsageReconciliationScan(db, claim, options = {}) {
|
|
964
|
+
const reconciliation = new MediaUsageReconciliationRepository(db);
|
|
965
|
+
let current = await reconciliation.findByIdentity(claim.collectionId, claim.runToken);
|
|
966
|
+
if (!current || current.leaseToken !== claim.leaseToken || current.phase !== "scan") return "deferred";
|
|
967
|
+
let fields;
|
|
968
|
+
let fieldFingerprint;
|
|
969
|
+
if (current.targetEpoch === null) {
|
|
970
|
+
const targetEpoch = await reconciliation.beginRun(claim);
|
|
971
|
+
if (targetEpoch === null) {
|
|
972
|
+
await reconciliation.release({
|
|
973
|
+
...claim,
|
|
974
|
+
delaySeconds: 30
|
|
975
|
+
});
|
|
976
|
+
return "deferred";
|
|
977
|
+
}
|
|
978
|
+
fields = await loadContentMediaUsageFields(db, claim.collectionSlug, claim.collectionId);
|
|
979
|
+
fieldFingerprint = await buildContentMediaUsageFieldFingerprint(fields);
|
|
980
|
+
const scanUpperId = fields.extractionFields.length === 0 ? null : await reconciliation.findScanUpperId(claim);
|
|
981
|
+
if (!await reconciliation.initializeScan({
|
|
982
|
+
claim,
|
|
983
|
+
targetEpoch,
|
|
984
|
+
fieldFingerprint,
|
|
985
|
+
scanUpperId
|
|
986
|
+
})) return "deferred";
|
|
987
|
+
current = await reconciliation.findByIdentity(claim.collectionId, claim.runToken);
|
|
988
|
+
if (!current) return "deferred";
|
|
989
|
+
} else {
|
|
990
|
+
fields = await loadContentMediaUsageFields(db, claim.collectionSlug, claim.collectionId);
|
|
991
|
+
fieldFingerprint = await buildContentMediaUsageFieldFingerprint(fields);
|
|
992
|
+
}
|
|
993
|
+
if (current.fieldFingerprint !== fieldFingerprint || current.targetEpoch === null) return "restart_required";
|
|
994
|
+
const contentIds = await reconciliation.findScanPage(current, 50);
|
|
995
|
+
if (contentIds.length === 0) {
|
|
996
|
+
if (options.releaseOnExhausted ?? true) await reconciliation.release({
|
|
997
|
+
...claim,
|
|
998
|
+
delaySeconds: 30
|
|
999
|
+
});
|
|
1000
|
+
return "exhausted";
|
|
1001
|
+
}
|
|
1002
|
+
await new MediaUsageWorkRepository(db).enqueueReconciliationPage({
|
|
1003
|
+
collectionId: claim.collectionId,
|
|
1004
|
+
collectionSlug: claim.collectionSlug,
|
|
1005
|
+
runToken: claim.runToken,
|
|
1006
|
+
leaseToken: claim.leaseToken,
|
|
1007
|
+
changeEpoch: current.targetEpoch,
|
|
1008
|
+
phase: "scan",
|
|
1009
|
+
contentIds
|
|
1010
|
+
});
|
|
1011
|
+
const nextCursor = contentIds.at(-1);
|
|
1012
|
+
if (!await reconciliation.checkpointScan({
|
|
1013
|
+
claim,
|
|
1014
|
+
targetEpoch: current.targetEpoch,
|
|
1015
|
+
previousCursor: current.scanCursor,
|
|
1016
|
+
nextCursor
|
|
1017
|
+
})) return "deferred";
|
|
1018
|
+
if (!await reconciliation.release({
|
|
1019
|
+
...claim,
|
|
1020
|
+
delaySeconds: 0
|
|
1021
|
+
})) return "deferred";
|
|
1022
|
+
return "advanced";
|
|
1023
|
+
}
|
|
1024
|
+
async function processClaimedReconciliation(db, claim) {
|
|
1025
|
+
const reconciliation = new MediaUsageReconciliationRepository(db);
|
|
1026
|
+
let current = await reconciliation.findByIdentity(claim.collectionId, claim.runToken);
|
|
1027
|
+
if (!current || current.leaseToken !== claim.leaseToken) return "claim_lost";
|
|
1028
|
+
if (current.targetEpoch !== null && !await reconciliation.ownsRun(claim, current.targetEpoch)) return restartReconciliation(db, reconciliation, claim, current);
|
|
1029
|
+
if (current.phase === "scan") {
|
|
1030
|
+
const outcome = await processClaimedMediaUsageReconciliationScan(db, claim, { releaseOnExhausted: false });
|
|
1031
|
+
if (outcome === "restart_required") {
|
|
1032
|
+
current = await reconciliation.findByIdentity(claim.collectionId, claim.runToken) ?? current;
|
|
1033
|
+
return restartReconciliation(db, reconciliation, claim, current);
|
|
1034
|
+
}
|
|
1035
|
+
if (outcome !== "exhausted") return outcome;
|
|
1036
|
+
current = await reconciliation.findByIdentity(claim.collectionId, claim.runToken) ?? current;
|
|
1037
|
+
const barrier = await reconciliation.findWorkBarrier(claim.collectionId);
|
|
1038
|
+
if (barrier.state === "failed") return failReconciliation(reconciliation, claim, barrier.errorCode, true);
|
|
1039
|
+
if (barrier.state === "pending") {
|
|
1040
|
+
await reconciliation.release({
|
|
1041
|
+
...claim,
|
|
1042
|
+
delaySeconds: 30
|
|
1043
|
+
});
|
|
1044
|
+
return "deferred";
|
|
139
1045
|
}
|
|
1046
|
+
if (current.targetEpoch === null || current.fieldFingerprint === null) return "claim_lost";
|
|
1047
|
+
const sourceUpperKey = await reconciliation.findSourceUpperKey(claim, current.targetEpoch);
|
|
1048
|
+
if (!await reconciliation.transitionToSources({
|
|
1049
|
+
claim,
|
|
1050
|
+
targetEpoch: current.targetEpoch,
|
|
1051
|
+
fieldFingerprint: current.fieldFingerprint,
|
|
1052
|
+
sourceUpperKey
|
|
1053
|
+
})) return "claim_lost";
|
|
1054
|
+
if (!await reconciliation.release({
|
|
1055
|
+
...claim,
|
|
1056
|
+
delaySeconds: 0
|
|
1057
|
+
})) return "claim_lost";
|
|
1058
|
+
return "advanced";
|
|
140
1059
|
}
|
|
1060
|
+
return processSourcePhase(db, reconciliation, claim, current);
|
|
1061
|
+
}
|
|
1062
|
+
async function processSourcePhase(db, reconciliation, claim, current) {
|
|
1063
|
+
if (current.targetEpoch === null || current.fieldFingerprint === null) return "claim_lost";
|
|
1064
|
+
const fields = await loadContentMediaUsageFields(db, claim.collectionSlug, claim.collectionId);
|
|
1065
|
+
const fieldFingerprint = await buildContentMediaUsageFieldFingerprint(fields);
|
|
1066
|
+
if (fieldFingerprint !== current.fieldFingerprint) return restartReconciliation(db, reconciliation, claim, current, fields, fieldFingerprint);
|
|
1067
|
+
const page = await reconciliation.findSourcePage(current, MEDIA_USAGE_RECONCILIATION_LIMITS.pageSize);
|
|
1068
|
+
if (page.length > 0) {
|
|
1069
|
+
if (page.some((source) => !source.contentId || source.sourceVariant !== "columns" && source.sourceVariant !== "draft_overlay")) return failReconciliation(reconciliation, claim, "MEDIA_USAGE_RECONCILIATION_INVALID_SOURCE", false);
|
|
1070
|
+
const contentIds = [...new Set(page.map((source) => source.contentId))];
|
|
1071
|
+
const enqueueIds = fields.extractionFields.length === 0 ? contentIds : await reconciliation.findMissingContentIds(claim.collectionSlug, contentIds);
|
|
1072
|
+
if (enqueueIds.length > 0) await new MediaUsageWorkRepository(db).enqueueReconciliationPage({
|
|
1073
|
+
collectionId: claim.collectionId,
|
|
1074
|
+
collectionSlug: claim.collectionSlug,
|
|
1075
|
+
runToken: claim.runToken,
|
|
1076
|
+
leaseToken: claim.leaseToken,
|
|
1077
|
+
changeEpoch: current.targetEpoch,
|
|
1078
|
+
phase: "sources",
|
|
1079
|
+
contentIds: enqueueIds
|
|
1080
|
+
});
|
|
1081
|
+
if (!await reconciliation.checkpointSources({
|
|
1082
|
+
claim,
|
|
1083
|
+
targetEpoch: current.targetEpoch,
|
|
1084
|
+
previousCursor: current.sourceCursor,
|
|
1085
|
+
nextCursor: page.at(-1).sourceKey
|
|
1086
|
+
})) return "claim_lost";
|
|
1087
|
+
if (!await reconciliation.release({
|
|
1088
|
+
...claim,
|
|
1089
|
+
delaySeconds: 0
|
|
1090
|
+
})) return "claim_lost";
|
|
1091
|
+
return "advanced";
|
|
1092
|
+
}
|
|
1093
|
+
const barrier = await reconciliation.findWorkBarrier(claim.collectionId);
|
|
1094
|
+
if (barrier.state === "failed") return failReconciliation(reconciliation, claim, barrier.errorCode, true);
|
|
1095
|
+
if (barrier.state === "pending") {
|
|
1096
|
+
await reconciliation.release({
|
|
1097
|
+
...claim,
|
|
1098
|
+
delaySeconds: 30
|
|
1099
|
+
});
|
|
1100
|
+
return "deferred";
|
|
1101
|
+
}
|
|
1102
|
+
if (!await reconciliation.finalizeCoverage({
|
|
1103
|
+
claim,
|
|
1104
|
+
targetEpoch: current.targetEpoch,
|
|
1105
|
+
fieldFingerprint,
|
|
1106
|
+
schemaVersion: CONTENT_SOURCE_SCHEMA_VERSION
|
|
1107
|
+
})) return "claim_lost";
|
|
1108
|
+
if (!await reconciliation.deleteFinalized(claim)) return "claim_lost";
|
|
1109
|
+
return "completed";
|
|
1110
|
+
}
|
|
1111
|
+
async function restartReconciliation(db, reconciliation, claim, current, fields, fieldFingerprint) {
|
|
1112
|
+
if (current.targetEpoch === null) return "claim_lost";
|
|
1113
|
+
const discoveredFields = fields ?? await loadContentMediaUsageFields(db, claim.collectionSlug, claim.collectionId);
|
|
1114
|
+
const fingerprint = fieldFingerprint ?? await buildContentMediaUsageFieldFingerprint(discoveredFields);
|
|
1115
|
+
const targetEpoch = await reconciliation.restartRun(claim, current.targetEpoch);
|
|
1116
|
+
if (targetEpoch === null) {
|
|
1117
|
+
await reconciliation.release({
|
|
1118
|
+
...claim,
|
|
1119
|
+
delaySeconds: 30
|
|
1120
|
+
});
|
|
1121
|
+
return "deferred";
|
|
1122
|
+
}
|
|
1123
|
+
const scanUpperId = discoveredFields.extractionFields.length === 0 ? null : await reconciliation.findScanUpperId(claim);
|
|
1124
|
+
if (!await reconciliation.restartScan({
|
|
1125
|
+
claim,
|
|
1126
|
+
previousEpoch: current.targetEpoch,
|
|
1127
|
+
targetEpoch,
|
|
1128
|
+
fieldFingerprint: fingerprint,
|
|
1129
|
+
scanUpperId
|
|
1130
|
+
})) return "claim_lost";
|
|
1131
|
+
if (!await reconciliation.release({
|
|
1132
|
+
...claim,
|
|
1133
|
+
delaySeconds: 0
|
|
1134
|
+
})) return "claim_lost";
|
|
1135
|
+
return "advanced";
|
|
1136
|
+
}
|
|
1137
|
+
async function failReconciliation(reconciliation, claim, errorCode, entryFailure) {
|
|
1138
|
+
if (!(entryFailure ? await reconciliation.recordEntryFailure(claim) : await reconciliation.recordFailure({
|
|
1139
|
+
collectionId: claim.collectionId,
|
|
1140
|
+
runToken: claim.runToken,
|
|
1141
|
+
leaseToken: claim.leaseToken,
|
|
1142
|
+
errorCode,
|
|
1143
|
+
retryDelaySeconds: 0,
|
|
1144
|
+
terminal: true
|
|
1145
|
+
}))) return "claim_lost";
|
|
1146
|
+
await reconciliation.finishFailedCoverage(claim.collectionId, claim.runToken);
|
|
1147
|
+
return "failed";
|
|
1148
|
+
}
|
|
1149
|
+
function retryDelaySeconds$1(attemptCount) {
|
|
1150
|
+
const exponential = Math.min(MEDIA_USAGE_RECONCILIATION_LIMITS.retryMaxSeconds, MEDIA_USAGE_RECONCILIATION_LIMITS.retryBaseSeconds * 2 ** attemptCount);
|
|
1151
|
+
const jitter = Math.floor(exponential * MEDIA_USAGE_RECONCILIATION_LIMITS.retryJitterRatio * Math.random());
|
|
1152
|
+
return Math.min(MEDIA_USAGE_RECONCILIATION_LIMITS.retryMaxSeconds, exponential + jitter);
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
//#endregion
|
|
1156
|
+
//#region src/media/usage/work-processor.ts
|
|
1157
|
+
const MEDIA_USAGE_WORK_PROCESSING_LIMITS = Object.freeze({
|
|
1158
|
+
candidatesPerTick: 4,
|
|
1159
|
+
jobsPerTick: 1,
|
|
1160
|
+
maxTickDurationMs: 5e3,
|
|
1161
|
+
leaseDurationSeconds: 60,
|
|
1162
|
+
maxAttempts: 5,
|
|
1163
|
+
retryBaseSeconds: 30,
|
|
1164
|
+
retryMaxSeconds: 900,
|
|
1165
|
+
retryJitterRatio: .25,
|
|
1166
|
+
ordinaryStatementsPerJob: 20
|
|
1167
|
+
});
|
|
1168
|
+
async function processMediaUsageWorkAfterWrite(db, collectionSlug, contentId) {
|
|
1169
|
+
if (!await isIncrementalCaptureActive(db)) return {
|
|
1170
|
+
outcome: "inactive",
|
|
1171
|
+
claimed: false
|
|
1172
|
+
};
|
|
1173
|
+
const repo = new MediaUsageWorkRepository(db);
|
|
1174
|
+
const work = await repo.findWorkForContent(collectionSlug, contentId);
|
|
1175
|
+
if (!work) return {
|
|
1176
|
+
outcome: "not_due",
|
|
1177
|
+
claimed: false
|
|
1178
|
+
};
|
|
1179
|
+
return processCandidate(db, repo, work);
|
|
1180
|
+
}
|
|
1181
|
+
async function processDueMediaUsageWork(db) {
|
|
1182
|
+
const startedAt = Date.now();
|
|
1183
|
+
const result = {
|
|
1184
|
+
candidateCount: 0,
|
|
1185
|
+
claimedCount: 0,
|
|
1186
|
+
completedCount: 0,
|
|
1187
|
+
retryCount: 0,
|
|
1188
|
+
failedCount: 0,
|
|
1189
|
+
supersededCount: 0,
|
|
1190
|
+
obsoleteCount: 0,
|
|
1191
|
+
durationMs: 0,
|
|
1192
|
+
admissionClosed: false
|
|
1193
|
+
};
|
|
1194
|
+
if (!await isIncrementalCaptureActive(db)) {
|
|
1195
|
+
result.durationMs = Date.now() - startedAt;
|
|
1196
|
+
return result;
|
|
1197
|
+
}
|
|
1198
|
+
const repo = new MediaUsageWorkRepository(db);
|
|
1199
|
+
const candidates = await repo.findDueWork(MEDIA_USAGE_WORK_PROCESSING_LIMITS.candidatesPerTick);
|
|
1200
|
+
result.candidateCount = candidates.length;
|
|
1201
|
+
for (const candidate of candidates) {
|
|
1202
|
+
if (result.claimedCount >= MEDIA_USAGE_WORK_PROCESSING_LIMITS.jobsPerTick || Date.now() - startedAt >= MEDIA_USAGE_WORK_PROCESSING_LIMITS.maxTickDurationMs) {
|
|
1203
|
+
result.admissionClosed = true;
|
|
1204
|
+
break;
|
|
1205
|
+
}
|
|
1206
|
+
const processed = await processCandidate(db, repo, candidate);
|
|
1207
|
+
if (processed.claimed) result.claimedCount++;
|
|
1208
|
+
if (processed.outcome === "completed") result.completedCount++;
|
|
1209
|
+
if (processed.outcome === "retry") result.retryCount++;
|
|
1210
|
+
if (processed.outcome === "failed") result.failedCount++;
|
|
1211
|
+
if (processed.outcome === "superseded") result.supersededCount++;
|
|
1212
|
+
if (processed.outcome === "obsolete") result.obsoleteCount++;
|
|
1213
|
+
}
|
|
1214
|
+
result.durationMs = Date.now() - startedAt;
|
|
1215
|
+
return result;
|
|
1216
|
+
}
|
|
1217
|
+
async function processCandidate(db, repo, candidate) {
|
|
1218
|
+
const claimed = await repo.claimWork({
|
|
1219
|
+
collectionId: candidate.collectionId,
|
|
1220
|
+
contentId: candidate.contentId,
|
|
1221
|
+
workVersion: candidate.workVersion,
|
|
1222
|
+
leaseDurationSeconds: MEDIA_USAGE_WORK_PROCESSING_LIMITS.leaseDurationSeconds
|
|
1223
|
+
});
|
|
1224
|
+
if (!claimed?.leaseToken) return {
|
|
1225
|
+
outcome: "claim_lost",
|
|
1226
|
+
claimed: false
|
|
1227
|
+
};
|
|
1228
|
+
const lease = {
|
|
1229
|
+
collectionId: claimed.collectionId,
|
|
1230
|
+
contentId: claimed.contentId,
|
|
1231
|
+
workVersion: claimed.workVersion,
|
|
1232
|
+
leaseToken: claimed.leaseToken
|
|
1233
|
+
};
|
|
1234
|
+
if (!await collectionIdentityIsCurrent(db, claimed.collectionId, claimed.collectionSlug)) return {
|
|
1235
|
+
outcome: await repo.completeWork(lease) ? "obsolete" : "superseded",
|
|
1236
|
+
claimed: true
|
|
1237
|
+
};
|
|
1238
|
+
const refresh = await refreshContentMediaUsageForWork(db, claimed.collectionId, claimed.collectionSlug, claimed.contentId);
|
|
1239
|
+
if (refresh.success) {
|
|
1240
|
+
const completed = await repo.completeWork(lease);
|
|
1241
|
+
if (completed) await new MediaUsageRepository(db).recordIncrementalSuccess({
|
|
1242
|
+
collectionId: claimed.collectionId,
|
|
1243
|
+
collectionSlug: claimed.collectionSlug
|
|
1244
|
+
});
|
|
1245
|
+
return {
|
|
1246
|
+
outcome: completed ? "completed" : "superseded",
|
|
1247
|
+
claimed: true
|
|
1248
|
+
};
|
|
1249
|
+
}
|
|
1250
|
+
if (!await collectionIdentityIsCurrent(db, claimed.collectionId, claimed.collectionSlug)) return {
|
|
1251
|
+
outcome: await repo.completeWork(lease) ? "obsolete" : "superseded",
|
|
1252
|
+
claimed: true
|
|
1253
|
+
};
|
|
1254
|
+
const errorCode = processingErrorCode(refresh.errorCode);
|
|
1255
|
+
if (errorCode === "MEDIA_USAGE_RESOURCE_LIMIT" || claimed.attemptCount + 1 >= MEDIA_USAGE_WORK_PROCESSING_LIMITS.maxAttempts) {
|
|
1256
|
+
const failed = await repo.failWork({
|
|
1257
|
+
...lease,
|
|
1258
|
+
errorCode
|
|
1259
|
+
});
|
|
1260
|
+
if (failed) await new MediaUsageRepository(db).recordIncrementalFailure({
|
|
1261
|
+
collectionId: claimed.collectionId,
|
|
1262
|
+
collectionSlug: claimed.collectionSlug,
|
|
1263
|
+
contentId: claimed.contentId,
|
|
1264
|
+
workVersion: claimed.workVersion,
|
|
1265
|
+
errorCode
|
|
1266
|
+
});
|
|
1267
|
+
return {
|
|
1268
|
+
outcome: failed ? "failed" : "superseded",
|
|
1269
|
+
claimed: true
|
|
1270
|
+
};
|
|
1271
|
+
}
|
|
1272
|
+
return {
|
|
1273
|
+
outcome: await repo.retryWork({
|
|
1274
|
+
...lease,
|
|
1275
|
+
errorCode,
|
|
1276
|
+
retryDelaySeconds: retryDelaySeconds(claimed.attemptCount)
|
|
1277
|
+
}) ? "retry" : "superseded",
|
|
1278
|
+
claimed: true
|
|
1279
|
+
};
|
|
1280
|
+
}
|
|
1281
|
+
async function isIncrementalCaptureActive(db) {
|
|
1282
|
+
return (await db.selectFrom("_emdash_media_usage_activation").select("state").where("task_key", "=", "incremental_capture").executeTakeFirst())?.state === "active";
|
|
1283
|
+
}
|
|
1284
|
+
async function collectionIdentityIsCurrent(db, collectionId, collectionSlug) {
|
|
1285
|
+
return await db.selectFrom("_emdash_collections").select("id").where("id", "=", collectionId).where("slug", "=", collectionSlug).executeTakeFirst() !== void 0;
|
|
1286
|
+
}
|
|
1287
|
+
function retryDelaySeconds(attemptCount) {
|
|
1288
|
+
const exponential = Math.min(MEDIA_USAGE_WORK_PROCESSING_LIMITS.retryMaxSeconds, MEDIA_USAGE_WORK_PROCESSING_LIMITS.retryBaseSeconds * 2 ** attemptCount);
|
|
1289
|
+
const jitter = Math.floor(exponential * MEDIA_USAGE_WORK_PROCESSING_LIMITS.retryJitterRatio * Math.random());
|
|
1290
|
+
return Math.min(MEDIA_USAGE_WORK_PROCESSING_LIMITS.retryMaxSeconds, exponential + jitter);
|
|
1291
|
+
}
|
|
1292
|
+
function processingErrorCode(errorCode) {
|
|
1293
|
+
if (errorCode === "DRAFT_REVISION_NOT_FOUND" || errorCode === "DRAFT_REVISION_MISMATCH" || errorCode === "DRAFT_REVISION_INVALID") return "MEDIA_USAGE_SNAPSHOT_FAILED";
|
|
1294
|
+
if (errorCode === "CONTENT_USAGE_GENERATION_CONFLICT") return "MEDIA_USAGE_GENERATION_CONFLICT";
|
|
1295
|
+
if (errorCode === "CONTENT_USAGE_RESOURCE_LIMIT") return "MEDIA_USAGE_RESOURCE_LIMIT";
|
|
1296
|
+
return "MEDIA_USAGE_PROCESSING_FAILED";
|
|
141
1297
|
}
|
|
142
1298
|
|
|
143
1299
|
//#endregion
|
|
@@ -153,6 +1309,227 @@ function createSandboxRunnerOptions(options, siteInfo) {
|
|
|
153
1309
|
};
|
|
154
1310
|
}
|
|
155
1311
|
|
|
1312
|
+
//#endregion
|
|
1313
|
+
//#region src/media/usage/cleanup.ts
|
|
1314
|
+
const MEDIA_USAGE_CLEANUP_CANDIDATE_LIMIT = 250;
|
|
1315
|
+
const MEDIA_USAGE_CLEANUP_DELETE_LIMIT = 50;
|
|
1316
|
+
const MEDIA_USAGE_CLEANUP_WRITE_LEASE_DELETE_LIMIT = 49;
|
|
1317
|
+
const MEDIA_USAGE_CLEANUP_INTERVAL_MS = 60 * 1e3;
|
|
1318
|
+
const MEDIA_USAGE_CLEANUP_LEASE_MS = 300 * 1e3;
|
|
1319
|
+
const MEDIA_USAGE_CLEANUP_SAFETY_WINDOW_MS = 3600 * 1e3;
|
|
1320
|
+
const MEDIA_USAGE_CLEANUP_TIME_BUDGET_MS = 5 * 1e3;
|
|
1321
|
+
/**
|
|
1322
|
+
* Reclaims a bounded window of obsolete media-usage occurrences.
|
|
1323
|
+
*
|
|
1324
|
+
* The persisted claim makes a cron tick single-flight across Worker isolates
|
|
1325
|
+
* and Node processes.
|
|
1326
|
+
*/
|
|
1327
|
+
async function cleanupMediaUsage(db) {
|
|
1328
|
+
const startedMs = Date.now();
|
|
1329
|
+
const canIssueStatement = () => withinBudget(startedMs);
|
|
1330
|
+
const repo = new MediaUsageRepository(db);
|
|
1331
|
+
const leaseToken = ulid();
|
|
1332
|
+
const claim = await repo.claimMediaUsageCleanup({
|
|
1333
|
+
leaseToken,
|
|
1334
|
+
leaseDurationSeconds: MEDIA_USAGE_CLEANUP_LEASE_MS / 1e3,
|
|
1335
|
+
nextEligibleDelaySeconds: MEDIA_USAGE_CLEANUP_INTERVAL_MS / 1e3,
|
|
1336
|
+
sweepSafetyWindowSeconds: MEDIA_USAGE_CLEANUP_SAFETY_WINDOW_MS / 1e3
|
|
1337
|
+
});
|
|
1338
|
+
if (!claim) return emptyResult("skipped", elapsedSince(startedMs));
|
|
1339
|
+
let candidateRows = 0;
|
|
1340
|
+
let deletedOrphans = 0;
|
|
1341
|
+
let deletedStale = 0;
|
|
1342
|
+
let deletedAbandoned = 0;
|
|
1343
|
+
let deletedWriteLeases = 0;
|
|
1344
|
+
let backlogLowerBound = 0;
|
|
1345
|
+
let scanHasMore = false;
|
|
1346
|
+
let nextCursor = claim.cursor;
|
|
1347
|
+
let sweepComplete = false;
|
|
1348
|
+
try {
|
|
1349
|
+
if (canIssueStatement()) deletedWriteLeases = await repo.deleteExpiredGenerationWriteLeases(MEDIA_USAGE_CLEANUP_WRITE_LEASE_DELETE_LIMIT, cleanupLease(leaseToken), canIssueStatement);
|
|
1350
|
+
if (canIssueStatement()) {
|
|
1351
|
+
const cutoff = claim.scanBeforeAt;
|
|
1352
|
+
const candidates = await repo.findMediaUsageCleanupCandidates({
|
|
1353
|
+
cutoff,
|
|
1354
|
+
cursor: claim.cursor,
|
|
1355
|
+
limit: MEDIA_USAGE_CLEANUP_CANDIDATE_LIMIT,
|
|
1356
|
+
cleanupLease: cleanupLease(leaseToken)
|
|
1357
|
+
});
|
|
1358
|
+
candidateRows = candidates.length;
|
|
1359
|
+
scanHasMore = candidates.length === MEDIA_USAGE_CLEANUP_CANDIDATE_LIMIT;
|
|
1360
|
+
const selected = selectCleanupCandidates(candidates, claim.claimedAt);
|
|
1361
|
+
backlogLowerBound = selected.orphanIds.length + selected.staleIds.length + selected.abandonedIds.length;
|
|
1362
|
+
const completedTargets = /* @__PURE__ */ new Set();
|
|
1363
|
+
let canContinue = true;
|
|
1364
|
+
if (canIssueStatement() && selected.orphanIds.length > 0) {
|
|
1365
|
+
deletedOrphans = await repo.deleteOrphanOccurrencesOlderThan(cutoff, selected.orphanIds.length, {
|
|
1366
|
+
candidateIds: selected.orphanIds,
|
|
1367
|
+
cleanupLease: cleanupLease(leaseToken),
|
|
1368
|
+
canIssueStatement
|
|
1369
|
+
});
|
|
1370
|
+
canContinue = deletedOrphans === selected.orphanIds.length;
|
|
1371
|
+
if (canContinue) completedTargets.add("orphan");
|
|
1372
|
+
}
|
|
1373
|
+
if (canContinue && canIssueStatement() && selected.staleIds.length > 0) {
|
|
1374
|
+
deletedStale = await repo.deleteStaleGenerationsOlderThan(cutoff, selected.staleIds.length, {
|
|
1375
|
+
candidateIds: selected.staleIds,
|
|
1376
|
+
cleanupLease: cleanupLease(leaseToken),
|
|
1377
|
+
canIssueStatement
|
|
1378
|
+
});
|
|
1379
|
+
canContinue = deletedStale === selected.staleIds.length;
|
|
1380
|
+
if (canContinue) completedTargets.add("stale");
|
|
1381
|
+
}
|
|
1382
|
+
if (canContinue && canIssueStatement() && selected.abandonedIds.length > 0) {
|
|
1383
|
+
deletedAbandoned = await repo.deleteAbandonedGenerationsOlderThan(cutoff, selected.abandonedIds.length, {
|
|
1384
|
+
candidateIds: selected.abandonedIds,
|
|
1385
|
+
cleanupLease: cleanupLease(leaseToken),
|
|
1386
|
+
canIssueStatement
|
|
1387
|
+
});
|
|
1388
|
+
if (deletedAbandoned === selected.abandonedIds.length) completedTargets.add("abandoned");
|
|
1389
|
+
}
|
|
1390
|
+
const hasIncompleteTargets = selected.entries.some((entry) => entry.target !== null && !completedTargets.has(entry.target));
|
|
1391
|
+
sweepComplete = !scanHasMore && selected.entries.length === candidates.length && !hasIncompleteTargets;
|
|
1392
|
+
nextCursor = sweepComplete ? null : cursorAfterCompletedCandidates(selected, completedTargets, claim.cursor);
|
|
1393
|
+
}
|
|
1394
|
+
const durationMs = elapsedSince(startedMs);
|
|
1395
|
+
return {
|
|
1396
|
+
status: await repo.completeMediaUsageCleanup({
|
|
1397
|
+
leaseToken,
|
|
1398
|
+
nextCursor,
|
|
1399
|
+
sweepComplete,
|
|
1400
|
+
candidateCount: candidateRows,
|
|
1401
|
+
deletedOrphans,
|
|
1402
|
+
deletedStale,
|
|
1403
|
+
deletedAbandoned,
|
|
1404
|
+
deletedWriteLeases,
|
|
1405
|
+
backlogLowerBound,
|
|
1406
|
+
scanHasMore,
|
|
1407
|
+
durationMs
|
|
1408
|
+
}) ? "completed" : "skipped",
|
|
1409
|
+
candidateRows,
|
|
1410
|
+
deletedRows: deletedOrphans + deletedStale + deletedAbandoned,
|
|
1411
|
+
deletedOrphans,
|
|
1412
|
+
deletedStale,
|
|
1413
|
+
deletedAbandoned,
|
|
1414
|
+
deletedWriteLeases,
|
|
1415
|
+
backlogLowerBound,
|
|
1416
|
+
scanHasMore,
|
|
1417
|
+
durationMs
|
|
1418
|
+
};
|
|
1419
|
+
} catch (error) {
|
|
1420
|
+
const durationMs = elapsedSince(startedMs);
|
|
1421
|
+
const failures = Math.min(claim.consecutiveFailures + 1, 5);
|
|
1422
|
+
try {
|
|
1423
|
+
await repo.failMediaUsageCleanup({
|
|
1424
|
+
leaseToken,
|
|
1425
|
+
retryDelaySeconds: failureDelayMs(failures) / 1e3,
|
|
1426
|
+
consecutiveFailures: failures,
|
|
1427
|
+
durationMs,
|
|
1428
|
+
errorCode: "MEDIA_USAGE_CLEANUP_FAILED"
|
|
1429
|
+
});
|
|
1430
|
+
} catch (failureError) {
|
|
1431
|
+
console.error("[media-usage-cleanup] Failed to record cleanup failure:", failureError);
|
|
1432
|
+
}
|
|
1433
|
+
console.error("[media-usage-cleanup] Cleanup failed:", error);
|
|
1434
|
+
return {
|
|
1435
|
+
...emptyResult("failed", durationMs),
|
|
1436
|
+
candidateRows,
|
|
1437
|
+
deletedRows: deletedOrphans + deletedStale + deletedAbandoned,
|
|
1438
|
+
deletedOrphans,
|
|
1439
|
+
deletedStale,
|
|
1440
|
+
deletedAbandoned,
|
|
1441
|
+
deletedWriteLeases,
|
|
1442
|
+
backlogLowerBound,
|
|
1443
|
+
scanHasMore
|
|
1444
|
+
};
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
function selectCleanupCandidates(candidates, activeLeaseAt) {
|
|
1448
|
+
const orphanIds = [];
|
|
1449
|
+
const staleIds = [];
|
|
1450
|
+
const abandonedIds = [];
|
|
1451
|
+
const entries = [];
|
|
1452
|
+
for (const candidate of candidates) {
|
|
1453
|
+
if (hasActiveWriteLease(candidate, activeLeaseAt)) {
|
|
1454
|
+
entries.push({
|
|
1455
|
+
candidate,
|
|
1456
|
+
target: null
|
|
1457
|
+
});
|
|
1458
|
+
continue;
|
|
1459
|
+
}
|
|
1460
|
+
const target = cleanupTarget(candidate);
|
|
1461
|
+
if (target === null) {
|
|
1462
|
+
entries.push({
|
|
1463
|
+
candidate,
|
|
1464
|
+
target: null
|
|
1465
|
+
});
|
|
1466
|
+
continue;
|
|
1467
|
+
}
|
|
1468
|
+
if (orphanIds.length + staleIds.length + abandonedIds.length >= MEDIA_USAGE_CLEANUP_DELETE_LIMIT) break;
|
|
1469
|
+
if (target === "orphan") orphanIds.push(candidate.id);
|
|
1470
|
+
if (target === "stale") staleIds.push(candidate.id);
|
|
1471
|
+
if (target === "abandoned") abandonedIds.push(candidate.id);
|
|
1472
|
+
entries.push({
|
|
1473
|
+
candidate,
|
|
1474
|
+
target
|
|
1475
|
+
});
|
|
1476
|
+
}
|
|
1477
|
+
return {
|
|
1478
|
+
orphanIds,
|
|
1479
|
+
staleIds,
|
|
1480
|
+
abandonedIds,
|
|
1481
|
+
entries
|
|
1482
|
+
};
|
|
1483
|
+
}
|
|
1484
|
+
function cleanupTarget(candidate) {
|
|
1485
|
+
if (candidate.currentGeneration === null) return "orphan";
|
|
1486
|
+
if (candidate.currentGeneration === candidate.generation || candidate.indexedAt === null) return null;
|
|
1487
|
+
return candidate.createdAt < candidate.indexedAt ? "stale" : "abandoned";
|
|
1488
|
+
}
|
|
1489
|
+
function hasActiveWriteLease(candidate, activeLeaseAt) {
|
|
1490
|
+
return candidate.writeLeaseExpiresAt !== null && candidate.writeLeaseExpiresAt > activeLeaseAt;
|
|
1491
|
+
}
|
|
1492
|
+
function cursorFor(candidate) {
|
|
1493
|
+
return {
|
|
1494
|
+
createdAt: candidate.createdAt,
|
|
1495
|
+
id: candidate.id
|
|
1496
|
+
};
|
|
1497
|
+
}
|
|
1498
|
+
function cursorAfterCompletedCandidates(selected, completedTargets, priorCursor) {
|
|
1499
|
+
let cursor = priorCursor;
|
|
1500
|
+
for (const entry of selected.entries) {
|
|
1501
|
+
if (entry.target !== null && !completedTargets.has(entry.target)) break;
|
|
1502
|
+
cursor = cursorFor(entry.candidate);
|
|
1503
|
+
}
|
|
1504
|
+
return cursor;
|
|
1505
|
+
}
|
|
1506
|
+
function emptyResult(status, durationMs) {
|
|
1507
|
+
return {
|
|
1508
|
+
status,
|
|
1509
|
+
candidateRows: 0,
|
|
1510
|
+
deletedRows: 0,
|
|
1511
|
+
deletedOrphans: 0,
|
|
1512
|
+
deletedStale: 0,
|
|
1513
|
+
deletedAbandoned: 0,
|
|
1514
|
+
deletedWriteLeases: 0,
|
|
1515
|
+
backlogLowerBound: 0,
|
|
1516
|
+
scanHasMore: false,
|
|
1517
|
+
durationMs
|
|
1518
|
+
};
|
|
1519
|
+
}
|
|
1520
|
+
function withinBudget(startedMs) {
|
|
1521
|
+
return elapsedSince(startedMs) < MEDIA_USAGE_CLEANUP_TIME_BUDGET_MS;
|
|
1522
|
+
}
|
|
1523
|
+
function elapsedSince(startedMs) {
|
|
1524
|
+
return Math.max(0, Date.now() - startedMs);
|
|
1525
|
+
}
|
|
1526
|
+
function failureDelayMs(consecutiveFailures) {
|
|
1527
|
+
return Math.min(2 ** (consecutiveFailures - 1), 15) * MEDIA_USAGE_CLEANUP_INTERVAL_MS;
|
|
1528
|
+
}
|
|
1529
|
+
function cleanupLease(leaseToken) {
|
|
1530
|
+
return { leaseToken };
|
|
1531
|
+
}
|
|
1532
|
+
|
|
156
1533
|
//#endregion
|
|
157
1534
|
//#region src/cleanup.ts
|
|
158
1535
|
/**
|
|
@@ -165,15 +1542,13 @@ function createSandboxRunnerOptions(options, siteInfo) {
|
|
|
165
1542
|
* Each subsystem cleanup is independent and non-fatal -- if one fails, the
|
|
166
1543
|
* rest still run. Failures are logged but never surface to callers.
|
|
167
1544
|
*/
|
|
168
|
-
/** Max revisions to keep per entry during periodic pruning */
|
|
169
1545
|
const REVISION_KEEP_COUNT = 50;
|
|
170
|
-
|
|
171
|
-
const REVISION_PRUNE_THRESHOLD = REVISION_KEEP_COUNT;
|
|
1546
|
+
const REVISION_PRUNE_BATCH_SIZE = 10;
|
|
172
1547
|
/**
|
|
173
1548
|
* Run all system cleanup tasks.
|
|
174
1549
|
*
|
|
175
|
-
* Safe to call frequently -- each
|
|
176
|
-
*
|
|
1550
|
+
* Safe to call frequently -- each subsystem tolerates repeated calls, and
|
|
1551
|
+
* repeated calls with nothing to clean are cheap.
|
|
177
1552
|
*
|
|
178
1553
|
* @param db - The database instance
|
|
179
1554
|
* @param storage - Optional storage backend for deleting orphaned files.
|
|
@@ -187,7 +1562,8 @@ async function runSystemCleanup(db, storage) {
|
|
|
187
1562
|
pendingUploads: -1,
|
|
188
1563
|
pendingUploadFiles: -1,
|
|
189
1564
|
uploadAttempts: -1,
|
|
190
|
-
revisionsPruned: -1
|
|
1565
|
+
revisionsPruned: -1,
|
|
1566
|
+
mediaUsage: -1
|
|
191
1567
|
};
|
|
192
1568
|
try {
|
|
193
1569
|
result.challenges = await cleanupExpiredChallenges(db);
|
|
@@ -230,29 +1606,24 @@ async function runSystemCleanup(db, storage) {
|
|
|
230
1606
|
console.error("[cleanup] Failed to clean media upload attempts:", error);
|
|
231
1607
|
}
|
|
232
1608
|
try {
|
|
233
|
-
result.revisionsPruned = await
|
|
1609
|
+
result.revisionsPruned = await pruneQueuedRevisions(db);
|
|
234
1610
|
} catch (error) {
|
|
235
1611
|
console.error("[cleanup] Failed to prune revisions:", error);
|
|
236
1612
|
}
|
|
1613
|
+
try {
|
|
1614
|
+
const mediaUsage = await cleanupMediaUsage(db);
|
|
1615
|
+
result.mediaUsage = mediaUsage.status === "failed" ? -1 : mediaUsage.deletedRows;
|
|
1616
|
+
} catch (error) {
|
|
1617
|
+
console.error("[cleanup] Failed to clean media usage:", error);
|
|
1618
|
+
}
|
|
237
1619
|
return result;
|
|
238
1620
|
}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
* them down to REVISION_KEEP_COUNT.
|
|
242
|
-
*/
|
|
243
|
-
async function pruneExcessiveRevisions(db) {
|
|
244
|
-
const entries = await sql`
|
|
245
|
-
SELECT collection, entry_id
|
|
246
|
-
FROM revisions
|
|
247
|
-
GROUP BY collection, entry_id
|
|
248
|
-
HAVING COUNT(*) > ${REVISION_PRUNE_THRESHOLD}
|
|
249
|
-
`.execute(db);
|
|
250
|
-
if (entries.rows.length === 0) return 0;
|
|
1621
|
+
async function pruneQueuedRevisions(db) {
|
|
1622
|
+
const queued = await db.selectFrom("_emdash_revision_prune_queue").selectAll().orderBy("revision_id").limit(REVISION_PRUNE_BATCH_SIZE).execute();
|
|
251
1623
|
const revisionRepo = new RevisionRepository(db);
|
|
252
1624
|
let totalPruned = 0;
|
|
253
|
-
for (const row of
|
|
254
|
-
|
|
255
|
-
totalPruned += pruned;
|
|
1625
|
+
for (const row of queued) try {
|
|
1626
|
+
totalPruned += await revisionRepo.pruneQueuedEntry(row.collection, row.entry_id, row.revision_id, REVISION_KEEP_COUNT);
|
|
256
1627
|
} catch (error) {
|
|
257
1628
|
console.error(`[cleanup] Failed to prune revisions for ${row.collection}/${row.entry_id}:`, error);
|
|
258
1629
|
}
|
|
@@ -330,7 +1701,8 @@ async function publishDueContent(db, options = {}) {
|
|
|
330
1701
|
const publishedAt = item.publishedAt == null ? item.scheduledAt ?? void 0 : void 0;
|
|
331
1702
|
const result = await doPublish(collection.slug, item.id, {
|
|
332
1703
|
publishedAt,
|
|
333
|
-
requireScheduledDue: true
|
|
1704
|
+
requireScheduledDue: true,
|
|
1705
|
+
expectedScheduledAt: item.scheduledAt ?? void 0
|
|
334
1706
|
});
|
|
335
1707
|
if (result.success) batch.push({
|
|
336
1708
|
collection: collection.slug,
|
|
@@ -440,6 +1812,16 @@ const DRAFT_ONLY_UPDATE_KEYS = new Set([
|
|
|
440
1812
|
"locale",
|
|
441
1813
|
"skipRevision"
|
|
442
1814
|
]);
|
|
1815
|
+
const MAX_DRAFT_STAGE_ATTEMPTS = 32;
|
|
1816
|
+
const LIST_COLUMN_FIELD_TYPES = new Set([
|
|
1817
|
+
"string",
|
|
1818
|
+
"number",
|
|
1819
|
+
"integer",
|
|
1820
|
+
"boolean",
|
|
1821
|
+
"datetime",
|
|
1822
|
+
"select",
|
|
1823
|
+
"multiSelect"
|
|
1824
|
+
]);
|
|
443
1825
|
/**
|
|
444
1826
|
* Convert a ContentItem to Record<string, unknown> for hook consumption.
|
|
445
1827
|
* Hooks receive the full item as a flat record.
|
|
@@ -523,6 +1905,49 @@ const marketplaceManifestCache = /* @__PURE__ */ new Map();
|
|
|
523
1905
|
/** Route metadata for sandboxed plugins: pluginId -> routeName -> RouteMeta */
|
|
524
1906
|
const sandboxedRouteMetaCache = /* @__PURE__ */ new Map();
|
|
525
1907
|
let sandboxRunner = null;
|
|
1908
|
+
const MEDIA_USAGE_MAINTENANCE_QUERY_RESERVATIONS = Object.freeze({
|
|
1909
|
+
entryWork: MEDIA_USAGE_WORK_PROCESSING_LIMITS.ordinaryStatementsPerJob,
|
|
1910
|
+
collectionDeletion: MEDIA_USAGE_COLLECTION_DELETION_LIMITS.maxQueriesPerTick,
|
|
1911
|
+
reconciliation: MEDIA_USAGE_RECONCILIATION_LIMITS.maxQueriesPerTick,
|
|
1912
|
+
maxClassQueries: Math.max(MEDIA_USAGE_WORK_PROCESSING_LIMITS.ordinaryStatementsPerJob, MEDIA_USAGE_COLLECTION_DELETION_LIMITS.maxQueriesPerTick, MEDIA_USAGE_RECONCILIATION_LIMITS.maxQueriesPerTick),
|
|
1913
|
+
eventCeiling: 40
|
|
1914
|
+
});
|
|
1915
|
+
async function runScheduledMediaUsageLane(db) {
|
|
1916
|
+
if ((getRequestContext()?.metrics?.dbCount ?? 0) + 1 + MEDIA_USAGE_MAINTENANCE_QUERY_RESERVATIONS.maxClassQueries > MEDIA_USAGE_MAINTENANCE_QUERY_RESERVATIONS.eventCeiling) return {
|
|
1917
|
+
outcome: "admission_closed",
|
|
1918
|
+
taskClass: null,
|
|
1919
|
+
turn: null
|
|
1920
|
+
};
|
|
1921
|
+
const activation = await db.updateTable("_emdash_media_usage_activation").set({ media_usage_maintenance_turn: sql`(media_usage_maintenance_turn + 1) % 3` }).where("task_key", "=", "incremental_capture").where("state", "=", "active").returning("media_usage_maintenance_turn").executeTakeFirst();
|
|
1922
|
+
if (!activation) return {
|
|
1923
|
+
outcome: "inactive",
|
|
1924
|
+
taskClass: null,
|
|
1925
|
+
turn: null
|
|
1926
|
+
};
|
|
1927
|
+
const turn = activation.media_usage_maintenance_turn;
|
|
1928
|
+
if (turn === 0) {
|
|
1929
|
+
await processDueMediaUsageWork(db);
|
|
1930
|
+
return {
|
|
1931
|
+
outcome: "processed",
|
|
1932
|
+
taskClass: "entry_work",
|
|
1933
|
+
turn
|
|
1934
|
+
};
|
|
1935
|
+
}
|
|
1936
|
+
if (turn === 1) {
|
|
1937
|
+
await processDueMediaUsageCollectionDeletions(db);
|
|
1938
|
+
return {
|
|
1939
|
+
outcome: "processed",
|
|
1940
|
+
taskClass: "collection_deletion",
|
|
1941
|
+
turn
|
|
1942
|
+
};
|
|
1943
|
+
}
|
|
1944
|
+
await processDueMediaUsageReconciliation(db);
|
|
1945
|
+
return {
|
|
1946
|
+
outcome: "processed",
|
|
1947
|
+
taskClass: "reconciliation",
|
|
1948
|
+
turn
|
|
1949
|
+
};
|
|
1950
|
+
}
|
|
526
1951
|
/**
|
|
527
1952
|
* EmDashRuntime - singleton per worker
|
|
528
1953
|
*/
|
|
@@ -574,6 +1999,8 @@ var EmDashRuntime = class EmDashRuntime {
|
|
|
574
1999
|
/** All plugins eligible for the hook pipeline (includes built-in plugins).
|
|
575
2000
|
* Stored so we can rebuild the pipeline when plugins are enabled/disabled. */
|
|
576
2001
|
allPipelinePlugins;
|
|
2002
|
+
/** Guards the once-per-process plugin storage-index sync. */
|
|
2003
|
+
storageIndexesSynced = false;
|
|
577
2004
|
/** Factory options for the hook pipeline context factory */
|
|
578
2005
|
pipelineFactoryOptions;
|
|
579
2006
|
/** Dependencies needed for exclusive hook resolution */
|
|
@@ -632,7 +2059,14 @@ var EmDashRuntime = class EmDashRuntime {
|
|
|
632
2059
|
* Returns the items promoted so callers can invalidate their cache tags.
|
|
633
2060
|
*/
|
|
634
2061
|
async publishScheduled() {
|
|
635
|
-
return
|
|
2062
|
+
return this.publishScheduledWithFence();
|
|
2063
|
+
}
|
|
2064
|
+
async publishScheduledWithFence(onPublished) {
|
|
2065
|
+
await assertMediaUsageActivationWriteAllowed(this.db);
|
|
2066
|
+
return publishDueContent(this.db, {
|
|
2067
|
+
publish: (collection, id, options) => this.handleContentPublish(collection, id, options),
|
|
2068
|
+
onPublished
|
|
2069
|
+
});
|
|
636
2070
|
}
|
|
637
2071
|
/**
|
|
638
2072
|
* Run the full scheduled-maintenance batch: cron tasks, scheduled
|
|
@@ -663,10 +2097,7 @@ var EmDashRuntime = class EmDashRuntime {
|
|
|
663
2097
|
}
|
|
664
2098
|
let published = [];
|
|
665
2099
|
try {
|
|
666
|
-
published = await
|
|
667
|
-
publish: (collection, id, opts) => this.handleContentPublish(collection, id, opts),
|
|
668
|
-
onPublished: options.onPublished
|
|
669
|
-
});
|
|
2100
|
+
published = await this.publishScheduledWithFence(options.onPublished);
|
|
670
2101
|
} catch (error) {
|
|
671
2102
|
console.error("[scheduled-publish] Sweep failed:", error);
|
|
672
2103
|
}
|
|
@@ -675,9 +2106,29 @@ var EmDashRuntime = class EmDashRuntime {
|
|
|
675
2106
|
} catch (error) {
|
|
676
2107
|
console.error("[cleanup] System cleanup failed:", error);
|
|
677
2108
|
}
|
|
2109
|
+
try {
|
|
2110
|
+
await this.syncPluginStorageIndexesOnce();
|
|
2111
|
+
} catch (error) {
|
|
2112
|
+
console.error("[plugins] Storage index sync failed:", error);
|
|
2113
|
+
}
|
|
678
2114
|
await maybeRunScheduledBackup(this.db, this.storage ?? void 0);
|
|
2115
|
+
await recordSchedulerHeartbeatSafely(this.db);
|
|
679
2116
|
return { published };
|
|
680
2117
|
}
|
|
2118
|
+
async runScheduledMediaUsageTasks() {
|
|
2119
|
+
return runScheduledMediaUsageLane(this.db);
|
|
2120
|
+
}
|
|
2121
|
+
/**
|
|
2122
|
+
* Materialize plugin-declared storage indexes, once per process.
|
|
2123
|
+
*
|
|
2124
|
+
* Called from the scheduler path, not from request handlers — configured
|
|
2125
|
+
* plugins have no install handler, so the tick is their only sync moment.
|
|
2126
|
+
*/
|
|
2127
|
+
async syncPluginStorageIndexesOnce() {
|
|
2128
|
+
if (this.storageIndexesSynced) return;
|
|
2129
|
+
this.storageIndexesSynced = true;
|
|
2130
|
+
await syncDeclaredStorageIndexes(this.db, [...this.allPipelinePlugins, ...marketplaceManifestCache.values()]);
|
|
2131
|
+
}
|
|
681
2132
|
/**
|
|
682
2133
|
* Stop the cron scheduler gracefully.
|
|
683
2134
|
* Call during worker shutdown or hot-reload.
|
|
@@ -819,7 +2270,8 @@ var EmDashRuntime = class EmDashRuntime {
|
|
|
819
2270
|
id: bundle.manifest.id,
|
|
820
2271
|
version: bundle.manifest.version,
|
|
821
2272
|
admin: bundle.manifest.admin,
|
|
822
|
-
mcp: bundle.manifest.mcp
|
|
2273
|
+
mcp: bundle.manifest.mcp,
|
|
2274
|
+
storage: bundle.manifest.storage
|
|
823
2275
|
});
|
|
824
2276
|
if (bundle.manifest.routes.length > 0) {
|
|
825
2277
|
const routeMetaMap = /* @__PURE__ */ new Map();
|
|
@@ -904,7 +2356,8 @@ var EmDashRuntime = class EmDashRuntime {
|
|
|
904
2356
|
id: bundle.manifest.id,
|
|
905
2357
|
version: bundle.manifest.version,
|
|
906
2358
|
admin: bundle.manifest.admin,
|
|
907
|
-
mcp: bundle.manifest.mcp
|
|
2359
|
+
mcp: bundle.manifest.mcp,
|
|
2360
|
+
storage: bundle.manifest.storage
|
|
908
2361
|
});
|
|
909
2362
|
if (bundle.manifest.routes.length > 0) {
|
|
910
2363
|
const routeMetaMap = /* @__PURE__ */ new Map();
|
|
@@ -1047,9 +2500,9 @@ var EmDashRuntime = class EmDashRuntime {
|
|
|
1047
2500
|
const seedHolder = getSeedHolder();
|
|
1048
2501
|
try {
|
|
1049
2502
|
await initWithLock(seedHolder.lock, () => seedHolder.done.has(seedKey) ? true : void 0, async () => {
|
|
1050
|
-
const { applySeed } = await import("../apply-
|
|
1051
|
-
const { loadSeed } = await import("../load-
|
|
1052
|
-
const { validateSeed } = await import("../validate-
|
|
2503
|
+
const { applySeed } = await import("../apply-D5zfeD09.mjs").then((n) => n.n);
|
|
2504
|
+
const { loadSeed } = await import("../load-DmvJrtxS.mjs").then((n) => n.r);
|
|
2505
|
+
const { validateSeed } = await import("../validate-BkqIG_Uj.mjs").then((n) => n.n);
|
|
1053
2506
|
const seed = await loadSeed();
|
|
1054
2507
|
if (validateSeed(seed).valid) {
|
|
1055
2508
|
await applySeed(db, seed, { onConflict: "skip" });
|
|
@@ -1130,6 +2583,7 @@ var EmDashRuntime = class EmDashRuntime {
|
|
|
1130
2583
|
const pipelineFactoryOptions = {
|
|
1131
2584
|
db,
|
|
1132
2585
|
getDb: resolveDb,
|
|
2586
|
+
beforeContentWrite: () => assertMediaUsageActivationWriteAllowed(resolveDb()),
|
|
1133
2587
|
storage: storage ?? void 0,
|
|
1134
2588
|
siteInfo
|
|
1135
2589
|
};
|
|
@@ -1180,10 +2634,19 @@ var EmDashRuntime = class EmDashRuntime {
|
|
|
1180
2634
|
if (deps.createScheduler) {
|
|
1181
2635
|
const scheduler = deps.createScheduler(cronExecutor);
|
|
1182
2636
|
cronScheduler = scheduler;
|
|
2637
|
+
const runMediaUsageMaintenance = async () => {
|
|
2638
|
+
const runtime = runtimeRef.current;
|
|
2639
|
+
if (runtime) await runtime.runScheduledMediaUsageTasks();
|
|
2640
|
+
else await runScheduledMediaUsageLane(db);
|
|
2641
|
+
};
|
|
1183
2642
|
scheduler.setSystemCleanup(async () => {
|
|
1184
2643
|
try {
|
|
1185
2644
|
const runtime = runtimeRef.current;
|
|
1186
|
-
|
|
2645
|
+
if (runtime) await runtime.publishScheduled();
|
|
2646
|
+
else {
|
|
2647
|
+
await assertMediaUsageActivationWriteAllowed(db);
|
|
2648
|
+
await publishDueContent(db);
|
|
2649
|
+
}
|
|
1187
2650
|
} catch (error) {
|
|
1188
2651
|
console.error("[scheduled-publish] Sweep failed:", error);
|
|
1189
2652
|
}
|
|
@@ -1192,8 +2655,20 @@ var EmDashRuntime = class EmDashRuntime {
|
|
|
1192
2655
|
} catch (error) {
|
|
1193
2656
|
console.error("[cleanup] System cleanup failed:", error);
|
|
1194
2657
|
}
|
|
2658
|
+
try {
|
|
2659
|
+
await runtimeRef.current?.syncPluginStorageIndexesOnce();
|
|
2660
|
+
} catch (error) {
|
|
2661
|
+
console.error("[plugins] Storage index sync failed:", error);
|
|
2662
|
+
}
|
|
1195
2663
|
await maybeRunScheduledBackup(db, storage ?? void 0);
|
|
2664
|
+
await recordSchedulerHeartbeatSafely(db);
|
|
2665
|
+
if (!scheduler.setMediaUsageMaintenance) try {
|
|
2666
|
+
await runMediaUsageMaintenance();
|
|
2667
|
+
} catch (error) {
|
|
2668
|
+
console.error("[media-usage] Scheduled maintenance failed:", error);
|
|
2669
|
+
}
|
|
1196
2670
|
});
|
|
2671
|
+
scheduler.setMediaUsageMaintenance?.(runMediaUsageMaintenance);
|
|
1197
2672
|
scheduler.start();
|
|
1198
2673
|
}
|
|
1199
2674
|
} catch (error) {
|
|
@@ -1353,6 +2828,7 @@ var EmDashRuntime = class EmDashRuntime {
|
|
|
1353
2828
|
if (!deps.sandboxEnabled) return sandboxedPluginCache;
|
|
1354
2829
|
if (!sandboxRunner && deps.createSandboxRunner) sandboxRunner = deps.createSandboxRunner(createSandboxRunnerOptions({
|
|
1355
2830
|
db,
|
|
2831
|
+
beforeContentWrite: () => assertMediaUsageActivationWriteAllowed(db),
|
|
1356
2832
|
mediaStorage: mediaStorage ? {
|
|
1357
2833
|
upload: (opts) => mediaStorage.upload({
|
|
1358
2834
|
key: opts.key,
|
|
@@ -1379,14 +2855,22 @@ var EmDashRuntime = class EmDashRuntime {
|
|
|
1379
2855
|
capabilities: entry.capabilities ?? [],
|
|
1380
2856
|
allowedHosts: entry.allowedHosts ?? [],
|
|
1381
2857
|
storage: entry.storage ?? {},
|
|
1382
|
-
hooks: [],
|
|
1383
|
-
routes: [],
|
|
2858
|
+
hooks: entry.hooks ?? [],
|
|
2859
|
+
routes: entry.routes ?? [],
|
|
1384
2860
|
admin: {},
|
|
1385
2861
|
mcp: entry.mcp
|
|
1386
2862
|
};
|
|
1387
2863
|
const plugin = await sandboxRunner.load(manifest, entry.code);
|
|
1388
2864
|
sandboxedPluginCache.set(pluginKey, plugin);
|
|
1389
2865
|
console.log(`EmDash: Loaded sandboxed plugin ${pluginKey} with capabilities: [${manifest.capabilities.join(", ")}]`);
|
|
2866
|
+
if (manifest.routes.length > 0) {
|
|
2867
|
+
const routeMetaMap = /* @__PURE__ */ new Map();
|
|
2868
|
+
for (const routeEntry of manifest.routes) {
|
|
2869
|
+
const normalized = normalizeManifestRoute(routeEntry);
|
|
2870
|
+
routeMetaMap.set(normalized.name, buildRouteMeta(normalized));
|
|
2871
|
+
}
|
|
2872
|
+
sandboxedRouteMetaCache.set(entry.id, routeMetaMap);
|
|
2873
|
+
} else sandboxedRouteMetaCache.delete(entry.id);
|
|
1390
2874
|
} catch (error) {
|
|
1391
2875
|
console.error(`EmDash: Failed to load sandboxed plugin ${entry.id}:`, error);
|
|
1392
2876
|
}
|
|
@@ -1410,6 +2894,7 @@ var EmDashRuntime = class EmDashRuntime {
|
|
|
1410
2894
|
static async loadInstalledSandboxedPlugins(source, db, storage, deps, cache, siteInfo) {
|
|
1411
2895
|
if (!sandboxRunner && deps.createSandboxRunner) sandboxRunner = deps.createSandboxRunner(createSandboxRunnerOptions({
|
|
1412
2896
|
db,
|
|
2897
|
+
beforeContentWrite: () => assertMediaUsageActivationWriteAllowed(db),
|
|
1413
2898
|
mediaStorage: {
|
|
1414
2899
|
upload: (opts) => storage.upload({
|
|
1415
2900
|
key: opts.key,
|
|
@@ -1443,7 +2928,8 @@ var EmDashRuntime = class EmDashRuntime {
|
|
|
1443
2928
|
id: bundle.manifest.id,
|
|
1444
2929
|
version: bundle.manifest.version,
|
|
1445
2930
|
admin: bundle.manifest.admin,
|
|
1446
|
-
mcp: bundle.manifest.mcp
|
|
2931
|
+
mcp: bundle.manifest.mcp,
|
|
2932
|
+
storage: bundle.manifest.storage
|
|
1447
2933
|
});
|
|
1448
2934
|
if (bundle.manifest.routes.length > 0) {
|
|
1449
2935
|
const routeMeta = /* @__PURE__ */ new Map();
|
|
@@ -1493,7 +2979,8 @@ var EmDashRuntime = class EmDashRuntime {
|
|
|
1493
2979
|
id: bundle.manifest.id,
|
|
1494
2980
|
version: bundle.manifest.version,
|
|
1495
2981
|
admin: bundle.manifest.admin,
|
|
1496
|
-
mcp: bundle.manifest.mcp
|
|
2982
|
+
mcp: bundle.manifest.mcp,
|
|
2983
|
+
storage: bundle.manifest.storage
|
|
1497
2984
|
});
|
|
1498
2985
|
if (bundle.manifest.routes.length > 0) {
|
|
1499
2986
|
const routeMeta = /* @__PURE__ */ new Map();
|
|
@@ -1609,12 +3096,33 @@ var EmDashRuntime = class EmDashRuntime {
|
|
|
1609
3096
|
if ((field.type === "repeater" || field.type === "file" || field.type === "image") && field.validation) entry.validation = { ...field.validation };
|
|
1610
3097
|
fields[field.slug] = entry;
|
|
1611
3098
|
}
|
|
3099
|
+
const configuredListColumns = collection.admin?.listColumns ?? [];
|
|
3100
|
+
const fieldTypes = new Map(collection.fields.map((field) => [field.slug, field.type]));
|
|
3101
|
+
const listColumns = [];
|
|
3102
|
+
for (const slug of configuredListColumns) {
|
|
3103
|
+
if (listColumns.includes(slug)) continue;
|
|
3104
|
+
const fieldType = fieldTypes.get(slug);
|
|
3105
|
+
if (!fieldType || !LIST_COLUMN_FIELD_TYPES.has(fieldType)) {
|
|
3106
|
+
console.warn(`EmDash: Ignoring unsupported or unknown list column "${slug}" in collection "${collection.slug}".`);
|
|
3107
|
+
continue;
|
|
3108
|
+
}
|
|
3109
|
+
if (listColumns.length >= MAX_COLLECTION_LIST_COLUMNS) {
|
|
3110
|
+
console.warn(`EmDash: Collection "${collection.slug}" declares more than ${MAX_COLLECTION_LIST_COLUMNS} list columns; extra columns are ignored.`);
|
|
3111
|
+
break;
|
|
3112
|
+
}
|
|
3113
|
+
listColumns.push(slug);
|
|
3114
|
+
}
|
|
1612
3115
|
manifestCollections[collection.slug] = {
|
|
1613
3116
|
label: collection.label,
|
|
1614
3117
|
labelSingular: collection.labelSingular || collection.label,
|
|
1615
3118
|
supports: collection.supports || [],
|
|
1616
3119
|
hasSeo: collection.hasSeo,
|
|
1617
3120
|
urlPattern: collection.urlPattern,
|
|
3121
|
+
routable: collection.routable !== false,
|
|
3122
|
+
titleField: collection.titleField,
|
|
3123
|
+
dateField: collection.dateField,
|
|
3124
|
+
...collection.hidden ? { hidden: true } : {},
|
|
3125
|
+
listColumns: listColumns.length > 0 ? listColumns : void 0,
|
|
1618
3126
|
fields
|
|
1619
3127
|
};
|
|
1620
3128
|
}
|
|
@@ -1674,21 +3182,33 @@ var EmDashRuntime = class EmDashRuntime {
|
|
|
1674
3182
|
};
|
|
1675
3183
|
}
|
|
1676
3184
|
let manifestTaxonomies = [];
|
|
3185
|
+
let taxonomyDefinitionLocales = [];
|
|
1677
3186
|
try {
|
|
1678
|
-
|
|
3187
|
+
const rows = await this.db.selectFrom("_emdash_taxonomy_defs").selectAll().orderBy("name").execute();
|
|
3188
|
+
taxonomyDefinitionLocales = rows.map((row) => row.locale);
|
|
3189
|
+
manifestTaxonomies = rows.map((row) => ({
|
|
3190
|
+
id: row.id,
|
|
1679
3191
|
name: row.name,
|
|
1680
3192
|
label: row.label,
|
|
1681
3193
|
labelSingular: row.label_singular ?? void 0,
|
|
1682
3194
|
hierarchical: row.hierarchical === 1,
|
|
1683
|
-
collections: parseStringArray(row.collections).toSorted()
|
|
3195
|
+
collections: parseStringArray(row.collections).toSorted(),
|
|
3196
|
+
locale: row.locale,
|
|
3197
|
+
translationGroup: row.translation_group ?? row.id
|
|
1684
3198
|
}));
|
|
1685
3199
|
} catch (error) {
|
|
1686
3200
|
console.debug("EmDash: Could not load taxonomy definitions:", error);
|
|
1687
3201
|
}
|
|
3202
|
+
try {
|
|
3203
|
+
const configuredLocales = virtualConfig?.i18n?.locales ?? getI18nConfig()?.locales ?? [];
|
|
3204
|
+
await warnAboutUnconfiguredTaxonomyLocales(this.db, configuredLocales, taxonomyDefinitionLocales);
|
|
3205
|
+
} catch (error) {
|
|
3206
|
+
console.warn("[i18n] taxonomy locale diagnostic failed:", error);
|
|
3207
|
+
}
|
|
1688
3208
|
const manifestHash = await hashString(JSON.stringify(manifestCollections) + JSON.stringify(manifestPlugins) + JSON.stringify(manifestTaxonomies));
|
|
1689
3209
|
const authMode = getAuthMode(this.config);
|
|
1690
3210
|
const authModeValue = authMode.type === "external" ? authMode.providerType : "passkey";
|
|
1691
|
-
const i18nConfig = virtualConfig?.i18n;
|
|
3211
|
+
const i18nConfig = virtualConfig?.i18n ?? getI18nConfig();
|
|
1692
3212
|
const i18n = i18nConfig && i18nConfig.locales && i18nConfig.locales.length > 1 ? {
|
|
1693
3213
|
defaultLocale: i18nConfig.defaultLocale,
|
|
1694
3214
|
locales: i18nConfig.locales
|
|
@@ -1704,6 +3224,10 @@ var EmDashRuntime = class EmDashRuntime {
|
|
|
1704
3224
|
taxonomies: manifestTaxonomies,
|
|
1705
3225
|
authMode: authModeValue,
|
|
1706
3226
|
i18n,
|
|
3227
|
+
contentLocale: {
|
|
3228
|
+
defaultLocale: i18nConfig?.defaultLocale ?? "en",
|
|
3229
|
+
implicit: i18nConfig === null
|
|
3230
|
+
},
|
|
1707
3231
|
marketplace: !!this.config.marketplace,
|
|
1708
3232
|
registry
|
|
1709
3233
|
};
|
|
@@ -1809,7 +3333,7 @@ var EmDashRuntime = class EmDashRuntime {
|
|
|
1809
3333
|
if (this.hooks.hasHooks("content:beforeSave")) processedData = (await this.hooks.runContentBeforeSave(body.data, collection, true)).content;
|
|
1810
3334
|
processedData = await this.runSandboxedBeforeSave(processedData, collection, true);
|
|
1811
3335
|
processedData = await this.normalizeMediaFields(collection, processedData);
|
|
1812
|
-
const { validateContentData } = await import("../validation-
|
|
3336
|
+
const { validateContentData } = await import("../validation-CiJb_X5o.mjs");
|
|
1813
3337
|
const validation = await validateContentData(this.db, collection, processedData, { partial: false });
|
|
1814
3338
|
if (!validation.ok) return {
|
|
1815
3339
|
success: false,
|
|
@@ -1826,7 +3350,6 @@ var EmDashRuntime = class EmDashRuntime {
|
|
|
1826
3350
|
return result;
|
|
1827
3351
|
}
|
|
1828
3352
|
async handleContentUpdate(collection, id, body) {
|
|
1829
|
-
const { ContentRepository } = await import("../content-CpfKV9QE.mjs").then((n) => n.n);
|
|
1830
3353
|
const repo = new ContentRepository(this.db);
|
|
1831
3354
|
const resolvedItem = await repo.findByIdOrSlug(collection, id, body.locale);
|
|
1832
3355
|
const resolvedId = resolvedItem?.id ?? id;
|
|
@@ -1853,7 +3376,7 @@ var EmDashRuntime = class EmDashRuntime {
|
|
|
1853
3376
|
if (this.hooks.hasHooks("content:beforeSave")) processedData = (await this.hooks.runContentBeforeSave(bodyWithoutRev.data, collection, false)).content;
|
|
1854
3377
|
processedData = await this.runSandboxedBeforeSave(processedData, collection, false);
|
|
1855
3378
|
processedData = await this.normalizeMediaFields(collection, processedData);
|
|
1856
|
-
const { validateContentData } = await import("../validation-
|
|
3379
|
+
const { validateContentData } = await import("../validation-CiJb_X5o.mjs");
|
|
1857
3380
|
const validation = await validateContentData(this.db, collection, processedData, { partial: true });
|
|
1858
3381
|
if (!validation.ok) return {
|
|
1859
3382
|
success: false,
|
|
@@ -1862,12 +3385,12 @@ var EmDashRuntime = class EmDashRuntime {
|
|
|
1862
3385
|
}
|
|
1863
3386
|
let usesDraftRevisions = false;
|
|
1864
3387
|
let draftStorageChanged = false;
|
|
1865
|
-
if (processedData)
|
|
3388
|
+
if (processedData) {
|
|
1866
3389
|
if ((await this.schemaRegistry.getCollectionWithFields(collection))?.supports?.includes("revisions")) {
|
|
1867
3390
|
usesDraftRevisions = true;
|
|
1868
3391
|
const revisionRepo = new RevisionRepository(this.db);
|
|
1869
|
-
|
|
1870
|
-
|
|
3392
|
+
let existing = await repo.findById(collection, resolvedId);
|
|
3393
|
+
for (let attempt = 0; existing && attempt < MAX_DRAFT_STAGE_ATTEMPTS; attempt++) {
|
|
1871
3394
|
let baseData;
|
|
1872
3395
|
if (existing.draftRevisionId) baseData = (await revisionRepo.findById(existing.draftRevisionId))?.data ?? existing.data;
|
|
1873
3396
|
else baseData = existing.data;
|
|
@@ -1876,37 +3399,64 @@ var EmDashRuntime = class EmDashRuntime {
|
|
|
1876
3399
|
...processedData
|
|
1877
3400
|
};
|
|
1878
3401
|
if (bodyWithoutRev.slug !== void 0) mergedData._slug = bodyWithoutRev.slug;
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
`.execute(this.db);
|
|
1896
|
-
draftStorageChanged = true;
|
|
1897
|
-
revisionRepo.pruneOldRevisions(collection, resolvedId, 50).catch(() => {});
|
|
3402
|
+
const revision = await revisionRepo.create({
|
|
3403
|
+
collection,
|
|
3404
|
+
entryId: resolvedId,
|
|
3405
|
+
data: mergedData,
|
|
3406
|
+
authorId: bodyWithoutRev.authorId ?? void 0
|
|
3407
|
+
});
|
|
3408
|
+
let staged;
|
|
3409
|
+
try {
|
|
3410
|
+
staged = await repo.replaceDraftRevision(collection, resolvedId, revision.id, existing);
|
|
3411
|
+
} catch (error) {
|
|
3412
|
+
try {
|
|
3413
|
+
await revisionRepo.deleteIfUnreferenced(collection, resolvedId, revision.id);
|
|
3414
|
+
} catch (cleanupError) {
|
|
3415
|
+
console.error(`[emdash] Failed to clean up unstaged revision ${revision.id}:`, cleanupError);
|
|
3416
|
+
}
|
|
3417
|
+
throw error;
|
|
1898
3418
|
}
|
|
3419
|
+
if (!staged) {
|
|
3420
|
+
try {
|
|
3421
|
+
await revisionRepo.deleteIfUnreferenced(collection, resolvedId, revision.id);
|
|
3422
|
+
} catch (cleanupError) {
|
|
3423
|
+
console.error(`[emdash] Failed to clean up unstaged revision ${revision.id}:`, cleanupError);
|
|
3424
|
+
}
|
|
3425
|
+
if (body._rev || attempt === MAX_DRAFT_STAGE_ATTEMPTS - 1) return {
|
|
3426
|
+
success: false,
|
|
3427
|
+
error: {
|
|
3428
|
+
code: "CONFLICT",
|
|
3429
|
+
message: new ContentMutationConflictError().message
|
|
3430
|
+
}
|
|
3431
|
+
};
|
|
3432
|
+
existing = await repo.findById(collection, resolvedId);
|
|
3433
|
+
continue;
|
|
3434
|
+
}
|
|
3435
|
+
draftStorageChanged = true;
|
|
3436
|
+
if (bodyWithoutRev.skipRevision && existing.draftRevisionId) try {
|
|
3437
|
+
await revisionRepo.deleteIfUnreferenced(collection, resolvedId, existing.draftRevisionId);
|
|
3438
|
+
} catch (error) {
|
|
3439
|
+
console.error(`[emdash] Failed to clean up superseded revision ${existing.draftRevisionId}:`, error);
|
|
3440
|
+
}
|
|
3441
|
+
else after(async () => {
|
|
3442
|
+
try {
|
|
3443
|
+
await revisionRepo.pruneQueuedEntry(collection, resolvedId, revision.id, 50);
|
|
3444
|
+
} catch (error) {
|
|
3445
|
+
console.error(`[revisions] Failed to prune revisions for ${collection}/${resolvedId}:`, error);
|
|
3446
|
+
}
|
|
3447
|
+
});
|
|
3448
|
+
break;
|
|
1899
3449
|
}
|
|
1900
3450
|
}
|
|
1901
|
-
}
|
|
1902
|
-
const
|
|
3451
|
+
}
|
|
3452
|
+
const liveMetaTouched = Object.entries(bodyWithoutRev).some(([key, value]) => value !== void 0 && !DRAFT_ONLY_UPDATE_KEYS.has(key));
|
|
3453
|
+
const result = usesDraftRevisions && !liveMetaTouched ? await handleContentGet(this.db, collection, resolvedId) : await handleContentUpdate(this.db, collection, resolvedId, {
|
|
1903
3454
|
...bodyWithoutRev,
|
|
1904
3455
|
data: usesDraftRevisions ? void 0 : processedData,
|
|
1905
3456
|
slug: usesDraftRevisions ? void 0 : bodyWithoutRev.slug,
|
|
1906
3457
|
authorId: bodyWithoutRev.authorId,
|
|
1907
3458
|
bylines: bodyWithoutRev.bylines
|
|
1908
3459
|
});
|
|
1909
|
-
const liveMetaTouched = Object.entries(bodyWithoutRev).some(([key, value]) => value !== void 0 && !DRAFT_ONLY_UPDATE_KEYS.has(key));
|
|
1910
3460
|
const liveContentChanged = usesDraftRevisions ? liveMetaTouched : Boolean(processedData || bodyWithoutRev.slug !== void 0 || liveMetaTouched);
|
|
1911
3461
|
const hydrated = await this.hydrateDraftData(result);
|
|
1912
3462
|
if (hydrated.success && hydrated.data) {
|
|
@@ -2086,25 +3636,50 @@ var EmDashRuntime = class EmDashRuntime {
|
|
|
2086
3636
|
return this.hydrateDraftData(result);
|
|
2087
3637
|
}
|
|
2088
3638
|
try {
|
|
3639
|
+
const contentRepo = new ContentRepository(this.db);
|
|
3640
|
+
const existing = await contentRepo.findById(revision.collection, revision.entryId);
|
|
3641
|
+
if (!existing) return {
|
|
3642
|
+
success: false,
|
|
3643
|
+
error: {
|
|
3644
|
+
code: "NOT_FOUND",
|
|
3645
|
+
message: `Content item not found: ${revision.entryId}`
|
|
3646
|
+
}
|
|
3647
|
+
};
|
|
2089
3648
|
const newDraft = await revisionRepo.create({
|
|
2090
3649
|
collection: revision.collection,
|
|
2091
3650
|
entryId: revision.entryId,
|
|
2092
3651
|
data: revision.data,
|
|
2093
3652
|
authorId: callerUserId
|
|
2094
3653
|
});
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
3654
|
+
try {
|
|
3655
|
+
if (!await contentRepo.replaceDraftRevision(revision.collection, revision.entryId, newDraft.id, existing)) throw new ContentMutationConflictError();
|
|
3656
|
+
} catch (error) {
|
|
3657
|
+
try {
|
|
3658
|
+
await revisionRepo.deleteIfUnreferenced(revision.collection, revision.entryId, newDraft.id);
|
|
3659
|
+
} catch (cleanupError) {
|
|
3660
|
+
console.error(`[emdash] Failed to clean up unrestored revision ${newDraft.id}:`, cleanupError);
|
|
3661
|
+
}
|
|
3662
|
+
throw error;
|
|
3663
|
+
}
|
|
3664
|
+
after(async () => {
|
|
3665
|
+
try {
|
|
3666
|
+
await revisionRepo.pruneQueuedEntry(revision.collection, revision.entryId, newDraft.id, 50);
|
|
3667
|
+
} catch (error) {
|
|
3668
|
+
console.error(`[revisions] Failed to prune revisions for ${revision.collection}/${revision.entryId}:`, error);
|
|
3669
|
+
}
|
|
3670
|
+
});
|
|
2103
3671
|
const refetched = await handleContentGet(this.db, revision.collection, revision.entryId);
|
|
2104
3672
|
const hydrated = await this.hydrateDraftData(refetched);
|
|
2105
3673
|
if (hydrated.success) await this.refreshContentUsageAfterSuccessfulWrite(revision.collection, [revision.entryId]);
|
|
2106
3674
|
return hydrated;
|
|
2107
3675
|
} catch (error) {
|
|
3676
|
+
if (error instanceof ContentMutationConflictError) return {
|
|
3677
|
+
success: false,
|
|
3678
|
+
error: {
|
|
3679
|
+
code: "CONFLICT",
|
|
3680
|
+
message: error.message
|
|
3681
|
+
}
|
|
3682
|
+
};
|
|
2108
3683
|
console.error("[emdash] revision restore failed:", error);
|
|
2109
3684
|
return {
|
|
2110
3685
|
success: false,
|
|
@@ -2117,13 +3692,16 @@ var EmDashRuntime = class EmDashRuntime {
|
|
|
2117
3692
|
}
|
|
2118
3693
|
async refreshContentUsageAfterSuccessfulWrite(collection, contentIds) {
|
|
2119
3694
|
for (const contentId of new Set(contentIds)) try {
|
|
3695
|
+
if ((await processMediaUsageWorkAfterWrite(this.db, collection, contentId)).outcome !== "inactive") return;
|
|
2120
3696
|
await refreshContentMediaUsageAfterWrite(this.db, collection, contentId);
|
|
2121
3697
|
} catch (error) {
|
|
2122
3698
|
console.error(`[media-usage] Failed after content write ${collection}/${contentId}:`, error);
|
|
3699
|
+
return;
|
|
2123
3700
|
}
|
|
2124
3701
|
}
|
|
2125
3702
|
async deleteContentUsageAfterSuccessfulPermanentDelete(collection, contentId) {
|
|
2126
3703
|
try {
|
|
3704
|
+
if ((await processMediaUsageWorkAfterWrite(this.db, collection, contentId)).outcome !== "inactive") return;
|
|
2127
3705
|
const result = await deleteContentMediaUsage(this.db, collection, contentId);
|
|
2128
3706
|
if (!result.success) console.error(`[media-usage] Usage delete for ${collection}/${contentId} finished with ${result.errorCode}`);
|
|
2129
3707
|
} catch (error) {
|
|
@@ -2169,7 +3747,7 @@ var EmDashRuntime = class EmDashRuntime {
|
|
|
2169
3747
|
if (!meta) return null;
|
|
2170
3748
|
return meta.admin?.settingsSchema ?? {};
|
|
2171
3749
|
}
|
|
2172
|
-
async handlePluginApiRoute(pluginId, _method, path, request) {
|
|
3750
|
+
async handlePluginApiRoute(pluginId, _method, path, request, user) {
|
|
2173
3751
|
if (!this.isPluginEnabled(pluginId)) return {
|
|
2174
3752
|
success: false,
|
|
2175
3753
|
error: {
|
|
@@ -2177,6 +3755,7 @@ var EmDashRuntime = class EmDashRuntime {
|
|
|
2177
3755
|
message: `Plugin not enabled: ${pluginId}`
|
|
2178
3756
|
}
|
|
2179
3757
|
};
|
|
3758
|
+
const caller = user ? toRouteCallerInfo(user) : void 0;
|
|
2180
3759
|
const trustedPlugin = this.configuredPlugins.find((p) => p.id === pluginId);
|
|
2181
3760
|
if (trustedPlugin && this.enabledPlugins.has(trustedPlugin.id)) {
|
|
2182
3761
|
const routeRegistry = new PluginRouteRegistry({
|
|
@@ -2187,17 +3766,15 @@ var EmDashRuntime = class EmDashRuntime {
|
|
|
2187
3766
|
});
|
|
2188
3767
|
routeRegistry.register(trustedPlugin);
|
|
2189
3768
|
const routeKey = path.replace(LEADING_SLASH_PATTERN, "");
|
|
2190
|
-
|
|
2191
|
-
try {
|
|
2192
|
-
body = await request.json();
|
|
2193
|
-
} catch {}
|
|
3769
|
+
const body = await parseRouteInput(request);
|
|
2194
3770
|
return routeRegistry.invoke(pluginId, routeKey, {
|
|
2195
3771
|
request,
|
|
2196
|
-
body
|
|
3772
|
+
body,
|
|
3773
|
+
user: caller
|
|
2197
3774
|
});
|
|
2198
3775
|
}
|
|
2199
3776
|
const sandboxedPlugin = this.findSandboxedPlugin(pluginId);
|
|
2200
|
-
if (sandboxedPlugin) return this.handleSandboxedRoute(sandboxedPlugin, path, request);
|
|
3777
|
+
if (sandboxedPlugin) return this.handleSandboxedRoute(sandboxedPlugin, path, request, caller);
|
|
2201
3778
|
return {
|
|
2202
3779
|
success: false,
|
|
2203
3780
|
error: {
|
|
@@ -2272,7 +3849,7 @@ var EmDashRuntime = class EmDashRuntime {
|
|
|
2272
3849
|
...tool.outputSchema ? { outputSchema: z.toJSONSchema(tool.outputSchema, { target: "draft-7" }) } : {}
|
|
2273
3850
|
})).toSorted((a, b) => a.name.localeCompare(b.name)));
|
|
2274
3851
|
}
|
|
2275
|
-
async handlePluginMcpTool(pluginId, toolName, route, input, actorId, request) {
|
|
3852
|
+
async handlePluginMcpTool(pluginId, toolName, route, input, actorId, request, caller) {
|
|
2276
3853
|
const requestMeta = extractRequestMeta(request, getTrustedProxyHeaders(this.config));
|
|
2277
3854
|
const audit = new AuditRepository(this.db);
|
|
2278
3855
|
const headers = new Headers(request.headers);
|
|
@@ -2283,7 +3860,7 @@ var EmDashRuntime = class EmDashRuntime {
|
|
|
2283
3860
|
headers,
|
|
2284
3861
|
body: JSON.stringify(input)
|
|
2285
3862
|
});
|
|
2286
|
-
const result = await this.handlePluginApiRoute(pluginId, "POST", route, internalRequest);
|
|
3863
|
+
const result = await this.handlePluginApiRoute(pluginId, "POST", route, internalRequest, caller);
|
|
2287
3864
|
await audit.log({
|
|
2288
3865
|
actorId,
|
|
2289
3866
|
actorIp: requestMeta.ip ?? void 0,
|
|
@@ -2332,7 +3909,8 @@ var EmDashRuntime = class EmDashRuntime {
|
|
|
2332
3909
|
}
|
|
2333
3910
|
if (!collectionInfo?.fields) return data;
|
|
2334
3911
|
const imageFields = collectionInfo.fields.filter((f) => f.type === "image" || f.type === "file");
|
|
2335
|
-
|
|
3912
|
+
const repeaterFields = collectionInfo.fields.filter((f) => f.type === "repeater" && Array.isArray(f.validation?.subFields));
|
|
3913
|
+
if (imageFields.length === 0 && repeaterFields.length === 0) return data;
|
|
2336
3914
|
const getProvider = (id) => this.getMediaProvider(id);
|
|
2337
3915
|
const result = { ...data };
|
|
2338
3916
|
for (const field of imageFields) {
|
|
@@ -2343,6 +3921,26 @@ var EmDashRuntime = class EmDashRuntime {
|
|
|
2343
3921
|
if (normalized) result[field.slug] = normalized;
|
|
2344
3922
|
} catch {}
|
|
2345
3923
|
}
|
|
3924
|
+
for (const field of repeaterFields) {
|
|
3925
|
+
const value = result[field.slug];
|
|
3926
|
+
if (!Array.isArray(value)) continue;
|
|
3927
|
+
const mediaSubFieldSlugs = (field.validation?.subFields ?? []).filter((sub) => sub.type === "image").map((sub) => sub.slug);
|
|
3928
|
+
if (mediaSubFieldSlugs.length === 0) continue;
|
|
3929
|
+
const items = value;
|
|
3930
|
+
result[field.slug] = await Promise.all(items.map(async (item) => {
|
|
3931
|
+
if (!isRecord(item)) return item;
|
|
3932
|
+
const normalizedItem = { ...item };
|
|
3933
|
+
for (const slug of mediaSubFieldSlugs) {
|
|
3934
|
+
const subValue = normalizedItem[slug];
|
|
3935
|
+
if (subValue == null) continue;
|
|
3936
|
+
try {
|
|
3937
|
+
const normalized = await normalizeMediaValue(subValue, getProvider);
|
|
3938
|
+
if (normalized) normalizedItem[slug] = normalized;
|
|
3939
|
+
} catch {}
|
|
3940
|
+
}
|
|
3941
|
+
return normalizedItem;
|
|
3942
|
+
}));
|
|
3943
|
+
}
|
|
2346
3944
|
return result;
|
|
2347
3945
|
}
|
|
2348
3946
|
async runSandboxedBeforeSave(content, collection, isNew) {
|
|
@@ -2491,12 +4089,9 @@ var EmDashRuntime = class EmDashRuntime {
|
|
|
2491
4089
|
runAfterUnscheduleHooks(content, collection) {
|
|
2492
4090
|
this.runDeferredContentHook("content:afterUnschedule", content, collection);
|
|
2493
4091
|
}
|
|
2494
|
-
async handleSandboxedRoute(plugin, path, request) {
|
|
4092
|
+
async handleSandboxedRoute(plugin, path, request, user) {
|
|
2495
4093
|
const routeName = path.replace(LEADING_SLASH_PATTERN, "");
|
|
2496
|
-
|
|
2497
|
-
try {
|
|
2498
|
-
body = await request.json();
|
|
2499
|
-
} catch {}
|
|
4094
|
+
const body = await parseRouteInput(request);
|
|
2500
4095
|
try {
|
|
2501
4096
|
const headers = sanitizeHeadersForSandbox(request.headers);
|
|
2502
4097
|
const meta = extractRequestMeta(request, this.config);
|
|
@@ -2506,11 +4101,21 @@ var EmDashRuntime = class EmDashRuntime {
|
|
|
2506
4101
|
url: request.url,
|
|
2507
4102
|
method: request.method,
|
|
2508
4103
|
headers,
|
|
2509
|
-
meta
|
|
4104
|
+
meta,
|
|
4105
|
+
user
|
|
2510
4106
|
})
|
|
2511
4107
|
};
|
|
2512
4108
|
} catch (error) {
|
|
2513
4109
|
console.error(`EmDash: Sandboxed plugin route error:`, error);
|
|
4110
|
+
const sandboxRouteError = getSandboxRouteErrorDetails(error);
|
|
4111
|
+
if (sandboxRouteError) return {
|
|
4112
|
+
success: false,
|
|
4113
|
+
status: sandboxRouteError.status,
|
|
4114
|
+
error: {
|
|
4115
|
+
code: sandboxRouteError.code,
|
|
4116
|
+
message: sandboxRouteError.message
|
|
4117
|
+
}
|
|
4118
|
+
};
|
|
2514
4119
|
return {
|
|
2515
4120
|
success: false,
|
|
2516
4121
|
error: {
|
|
@@ -2629,6 +4234,26 @@ function createPublicMediaUrlResolver(storage) {
|
|
|
2629
4234
|
* middleware.ts.
|
|
2630
4235
|
*/
|
|
2631
4236
|
const ASTRO_COOKIES_SYMBOL = Symbol.for("astro.cookies");
|
|
4237
|
+
/** Hold the real adapter close behind both response and deferred-task completion. */
|
|
4238
|
+
function coordinateScopedDbLifecycle(scoped) {
|
|
4239
|
+
if (!scoped.close) return { lifecycle: scoped };
|
|
4240
|
+
const close = scoped.close;
|
|
4241
|
+
const deferredTasks = createDeferredTaskTracker(() => {
|
|
4242
|
+
try {
|
|
4243
|
+
close();
|
|
4244
|
+
} catch (error) {
|
|
4245
|
+
console.error("[emdash] request-scoped db close failed:", error);
|
|
4246
|
+
}
|
|
4247
|
+
});
|
|
4248
|
+
return {
|
|
4249
|
+
closed: deferredTasks.settled,
|
|
4250
|
+
deferredTasks,
|
|
4251
|
+
lifecycle: {
|
|
4252
|
+
commit: scoped.commit,
|
|
4253
|
+
close: deferredTasks.settle
|
|
4254
|
+
}
|
|
4255
|
+
};
|
|
4256
|
+
}
|
|
2632
4257
|
/**
|
|
2633
4258
|
* Run a request-scoped db's `close()` once the response body has finished
|
|
2634
4259
|
* streaming. Astro streams HTML and components issue DB queries during that
|
|
@@ -2668,9 +4293,9 @@ function wrapResponseForScopedClose(response, close) {
|
|
|
2668
4293
|
* Run the request body under a request-scoped db, then settle its lifecycle:
|
|
2669
4294
|
* `commit()` runs before the response is returned (so per-request state like a
|
|
2670
4295
|
* D1 bookmark cookie is persisted in the headers, even if render throws), while
|
|
2671
|
-
* `close()` (if any) is deferred to
|
|
2672
|
-
* isn't torn down mid-render. On error
|
|
2673
|
-
* before rethrowing so it never leaks.
|
|
4296
|
+
* `close()` (if any) is deferred to lifecycle settlement so a
|
|
4297
|
+
* connection-backed adapter isn't torn down mid-render or mid-task. On error
|
|
4298
|
+
* the lifecycle is settled before rethrowing so it never leaks.
|
|
2674
4299
|
*
|
|
2675
4300
|
* On the error path both `commit()` and `close()` are defended: a throw from
|
|
2676
4301
|
* either is logged and swallowed so it can't replace the propagating render
|
|
@@ -2870,12 +4495,6 @@ function createPublicPluginApiRouteHandler(runtime) {
|
|
|
2870
4495
|
//#endregion
|
|
2871
4496
|
//#region src/astro/middleware.ts
|
|
2872
4497
|
/**
|
|
2873
|
-
* EmDash middleware
|
|
2874
|
-
*
|
|
2875
|
-
* Thin wrapper that initializes EmDashRuntime and attaches it to locals.
|
|
2876
|
-
* All heavy lifting happens in EmDashRuntime.
|
|
2877
|
-
*/
|
|
2878
|
-
/**
|
|
2879
4498
|
* Runtime init lock reclaim deadline. Must be strictly larger than the db
|
|
2880
4499
|
* init deadline: this lock wraps EmDashRuntime.create() → getDatabase() →
|
|
2881
4500
|
* the db init lock, and equal deadlines would let this outer lock reclaim
|
|
@@ -3019,6 +4638,15 @@ async function runScheduledTasks(options = {}) {
|
|
|
3019
4638
|
if (!config) return { published: [] };
|
|
3020
4639
|
return runOutsideRequest(config, (runtime) => runtime.runScheduledTasks(options));
|
|
3021
4640
|
}
|
|
4641
|
+
async function runScheduledMediaUsageTasks() {
|
|
4642
|
+
const config = getConfig();
|
|
4643
|
+
if (!config) return {
|
|
4644
|
+
outcome: "inactive",
|
|
4645
|
+
taskClass: null,
|
|
4646
|
+
turn: null
|
|
4647
|
+
};
|
|
4648
|
+
return runOutsideRequest(config, (runtime) => runtime.runScheduledMediaUsageTasks());
|
|
4649
|
+
}
|
|
3022
4650
|
/**
|
|
3023
4651
|
* Run a callback against the EmDash runtime outside any HTTP request — from a
|
|
3024
4652
|
* Cloudflare Queue consumer, a `scheduled()` handler, or any other
|
|
@@ -3062,49 +4690,71 @@ async function withEmDashRuntime(run) {
|
|
|
3062
4690
|
/**
|
|
3063
4691
|
* Shared plumbing for request-free entry points (`runScheduledTasks`,
|
|
3064
4692
|
* `withEmDashRuntime`): resolve the runtime singleton, then run the callback
|
|
3065
|
-
* under an event-scoped db
|
|
4693
|
+
* under an event-scoped db when the adapter needs one.
|
|
3066
4694
|
*
|
|
3067
4695
|
* Connection-backed adapters (e.g. Postgres over Hyperdrive) cannot reuse
|
|
3068
4696
|
* the per-isolate singleton from a platform event: its socket belongs to the
|
|
3069
4697
|
* request that opened it, and workerd rejects cross-event I/O. Open an
|
|
3070
4698
|
* event-scoped connection and run the callback under it in ALS — the
|
|
3071
4699
|
* runtime's db getter, the cron executor, and plugin contexts all resolve
|
|
3072
|
-
* the connection from ALS — then close it
|
|
3073
|
-
*
|
|
3074
|
-
*
|
|
4700
|
+
* the connection from ALS — then close it when required. Stateless adapters
|
|
4701
|
+
* that need primary routing can return a close-less scope; adapters with no
|
|
4702
|
+
* event scoping return null and keep using the singleton.
|
|
3075
4703
|
*/
|
|
3076
4704
|
async function runOutsideRequest(config, fn) {
|
|
3077
|
-
|
|
4705
|
+
if (getRequestContext()) return runOutsideRequestWithRuntime(config, await getRuntime(config), fn);
|
|
4706
|
+
const deferredTasks = createDeferredTaskTracker(() => {});
|
|
4707
|
+
return runWithContext({
|
|
4708
|
+
editMode: false,
|
|
4709
|
+
metrics: createRequestMetrics(performance.now()),
|
|
4710
|
+
deferredTasks
|
|
4711
|
+
}, async () => {
|
|
4712
|
+
return runOutsideRequestWithRuntime(config, await (async () => {
|
|
4713
|
+
try {
|
|
4714
|
+
return await getRuntime(config);
|
|
4715
|
+
} finally {
|
|
4716
|
+
deferredTasks.settle();
|
|
4717
|
+
await deferredTasks.settled;
|
|
4718
|
+
}
|
|
4719
|
+
})(), fn);
|
|
4720
|
+
});
|
|
4721
|
+
}
|
|
4722
|
+
async function runOutsideRequestWithRuntime(config, runtime, fn) {
|
|
3078
4723
|
const scoped = createRequestScopedDb$1({
|
|
3079
4724
|
config: config.database?.config,
|
|
3080
4725
|
isAuthenticated: false,
|
|
3081
4726
|
isWrite: true,
|
|
4727
|
+
canUseCachedBinding: false,
|
|
3082
4728
|
cookies: NOOP_COOKIE_JAR,
|
|
3083
4729
|
url: CRON_EVENT_URL
|
|
3084
4730
|
});
|
|
3085
|
-
if (!scoped
|
|
4731
|
+
if (!scoped) return fn(runtime);
|
|
4732
|
+
const { closed, deferredTasks, lifecycle } = coordinateScopedDbLifecycle(scoped);
|
|
3086
4733
|
const parent = getRequestContext();
|
|
3087
4734
|
const ctx = parent ? {
|
|
3088
4735
|
...parent,
|
|
3089
|
-
db: scoped.db
|
|
4736
|
+
db: scoped.db,
|
|
4737
|
+
deferredTasks
|
|
3090
4738
|
} : {
|
|
3091
4739
|
editMode: false,
|
|
3092
4740
|
db: scoped.db,
|
|
3093
|
-
metrics: createRequestMetrics(performance.now())
|
|
4741
|
+
metrics: createRequestMetrics(performance.now()),
|
|
4742
|
+
deferredTasks
|
|
3094
4743
|
};
|
|
3095
4744
|
try {
|
|
3096
4745
|
return await runWithContext(ctx, () => fn(runtime));
|
|
3097
4746
|
} finally {
|
|
3098
4747
|
try {
|
|
3099
|
-
|
|
4748
|
+
lifecycle.commit();
|
|
3100
4749
|
} catch (error) {
|
|
3101
4750
|
console.error("[emdash] event-scoped db commit failed:", error);
|
|
3102
4751
|
}
|
|
3103
4752
|
try {
|
|
3104
|
-
|
|
4753
|
+
lifecycle.close?.();
|
|
3105
4754
|
} catch (error) {
|
|
3106
4755
|
console.error("[emdash] event-scoped db close failed:", error);
|
|
3107
4756
|
}
|
|
4757
|
+
await closed;
|
|
3108
4758
|
}
|
|
3109
4759
|
}
|
|
3110
4760
|
/**
|
|
@@ -3203,12 +4853,39 @@ function createRequestScopedDb$1(opts) {
|
|
|
3203
4853
|
if (typeof createRequestScopedDb !== "function") return null;
|
|
3204
4854
|
return createRequestScopedDb(opts);
|
|
3205
4855
|
}
|
|
4856
|
+
const buildDate = buildTime ? new Date(buildTime) : null;
|
|
4857
|
+
/**
|
|
4858
|
+
* Fold the build timestamp into the route cache validator.
|
|
4859
|
+
*
|
|
4860
|
+
* `CacheHint.lastModified` describes the content, but the response also depends
|
|
4861
|
+
* on the build: `/_astro/*` names are content-hashed, and a deployment only
|
|
4862
|
+
* serves its own. Without the build dimension a code-only deploy answers a
|
|
4863
|
+
* returning visitor's conditional request with 304, leaving them on HTML whose
|
|
4864
|
+
* assets 404.
|
|
4865
|
+
*
|
|
4866
|
+
* Prerendered pages are served by the host's static layer, which manages its
|
|
4867
|
+
* own validators — only on-demand responses need the build dimension.
|
|
4868
|
+
*
|
|
4869
|
+
* Only forward moves are covered. `Last-Modified` expresses newer, not
|
|
4870
|
+
* different, so after a rollback the earlier build still answers a conditional
|
|
4871
|
+
* request with 304 and the browser stays on the newer build's HTML.
|
|
4872
|
+
*
|
|
4873
|
+
* Must run before next(): Astro keeps the later of two dates, so a route's own
|
|
4874
|
+
* hint still wins when content is newer, and a route that opts out with
|
|
4875
|
+
* `Astro.cache.set(false)` stays opted out — calling set() afterwards would
|
|
4876
|
+
* clear that opt-out.
|
|
4877
|
+
*/
|
|
4878
|
+
function applyBuildValidator(context) {
|
|
4879
|
+
if (context.isPrerendered || !buildDate || !context.cache?.enabled) return;
|
|
4880
|
+
context.cache.set({ lastModified: buildDate });
|
|
4881
|
+
}
|
|
3206
4882
|
const onRequest = defineMiddleware(async (context, next) => {
|
|
3207
4883
|
const { request, locals, cookies } = context;
|
|
3208
4884
|
const url = context.url;
|
|
3209
4885
|
if (!url.pathname.startsWith("/_emdash") && virtualConfig?.authProviders) {
|
|
3210
4886
|
if (virtualConfig.authProviders.some((p) => p.routes?.some((r) => r.pattern && url.pathname === r.pattern))) return finalizeResponse(await next());
|
|
3211
4887
|
}
|
|
4888
|
+
applyBuildValidator(context);
|
|
3212
4889
|
const queryRecorder = isInstrumentationEnabled() ? createRecorder(url.pathname, request.method, request.headers.get("x-perf-phase") ?? "default") : void 0;
|
|
3213
4890
|
const metrics = createRequestMetrics(performance.now());
|
|
3214
4891
|
const run = async () => {
|
|
@@ -3220,6 +4897,9 @@ const onRequest = defineMiddleware(async (context, next) => {
|
|
|
3220
4897
|
const hasSessionCookie = cookies.get("astro-session") !== void 0;
|
|
3221
4898
|
const sessionUser = context.isPrerendered || !hasSessionCookie ? null : await resolveSessionUser(context.session);
|
|
3222
4899
|
const hasBearerAuth = (request.headers.get("authorization") ?? "").toLowerCase().startsWith("bearer ");
|
|
4900
|
+
const isWrite = request.method !== "GET" && request.method !== "HEAD";
|
|
4901
|
+
const isAuthenticated = !!sessionUser || hasBearerAuth;
|
|
4902
|
+
const canUseCachedBinding = !isAuthenticated && !isWrite && !playgroundDb && !isEmDashRoute && !hasEditCookie && !hasPreviewToken;
|
|
3223
4903
|
if (!isEmDashRoute && !isPublicRuntimeRoute && !hasEditCookie && !hasPreviewToken) {
|
|
3224
4904
|
if (!sessionUser && !playgroundDb) {
|
|
3225
4905
|
const timings = [];
|
|
@@ -3227,7 +4907,7 @@ const onRequest = defineMiddleware(async (context, next) => {
|
|
|
3227
4907
|
if (!isSetupVerified() && !context.isPrerendered) {
|
|
3228
4908
|
const t0 = performance.now();
|
|
3229
4909
|
try {
|
|
3230
|
-
const { getDb } = await import("../loader-
|
|
4910
|
+
const { getDb } = await import("../loader-DLBqRiGP.mjs").then((n) => n.c);
|
|
3231
4911
|
await (await getDb()).selectFrom("_emdash_migrations").selectAll().limit(1).execute();
|
|
3232
4912
|
markSetupVerified();
|
|
3233
4913
|
} catch (error) {
|
|
@@ -3267,12 +4947,15 @@ const onRequest = defineMiddleware(async (context, next) => {
|
|
|
3267
4947
|
});
|
|
3268
4948
|
for (const sub of initSubTimings) timings.push(sub);
|
|
3269
4949
|
}
|
|
4950
|
+
const lastContentWriteAt = canUseCachedBinding && config?.database?.needsLastContentWriteAt ? await getLastContentWriteAt() : void 0;
|
|
3270
4951
|
const anonScoped = createRequestScopedDb$1({
|
|
3271
4952
|
config: config?.database?.config,
|
|
3272
|
-
isAuthenticated
|
|
3273
|
-
isWrite
|
|
4953
|
+
isAuthenticated,
|
|
4954
|
+
isWrite,
|
|
4955
|
+
canUseCachedBinding,
|
|
3274
4956
|
cookies,
|
|
3275
|
-
url
|
|
4957
|
+
url,
|
|
4958
|
+
lastContentWriteAt
|
|
3276
4959
|
});
|
|
3277
4960
|
const runAnon = async () => {
|
|
3278
4961
|
const t0 = performance.now();
|
|
@@ -3291,19 +4974,22 @@ const onRequest = defineMiddleware(async (context, next) => {
|
|
|
3291
4974
|
return wrapBodyForStreamMetrics(finalizeResponse(response, timings));
|
|
3292
4975
|
};
|
|
3293
4976
|
if (anonScoped) {
|
|
4977
|
+
const { deferredTasks, lifecycle } = coordinateScopedDbLifecycle(anonScoped);
|
|
3294
4978
|
const parent = getRequestContext();
|
|
3295
4979
|
const ctx = parent ? {
|
|
3296
4980
|
...parent,
|
|
3297
|
-
db: anonScoped.db
|
|
4981
|
+
db: anonScoped.db,
|
|
4982
|
+
deferredTasks
|
|
3298
4983
|
} : {
|
|
3299
4984
|
editMode: false,
|
|
3300
4985
|
db: anonScoped.db,
|
|
3301
|
-
metrics
|
|
4986
|
+
metrics,
|
|
4987
|
+
deferredTasks
|
|
3302
4988
|
};
|
|
3303
4989
|
const acceptsHtml = (request.headers.get("accept") ?? "").split(",", 1)[0].trim().startsWith("text/html");
|
|
3304
4990
|
return runWithContext(ctx, async () => {
|
|
3305
4991
|
if (acceptsHtml) after(() => prefetchLayoutData());
|
|
3306
|
-
return finishScoped(
|
|
4992
|
+
return finishScoped(lifecycle, runAnon);
|
|
3307
4993
|
});
|
|
3308
4994
|
}
|
|
3309
4995
|
return runAnon();
|
|
@@ -3391,12 +5077,15 @@ const onRequest = defineMiddleware(async (context, next) => {
|
|
|
3391
5077
|
} catch (error) {
|
|
3392
5078
|
console.error("EmDash middleware error:", error);
|
|
3393
5079
|
}
|
|
5080
|
+
const lastContentWriteAt = canUseCachedBinding && config?.database?.needsLastContentWriteAt ? await getLastContentWriteAt() : void 0;
|
|
3394
5081
|
const scoped = createRequestScopedDb$1({
|
|
3395
5082
|
config: config?.database?.config,
|
|
3396
|
-
isAuthenticated
|
|
3397
|
-
isWrite
|
|
5083
|
+
isAuthenticated,
|
|
5084
|
+
isWrite,
|
|
5085
|
+
canUseCachedBinding,
|
|
3398
5086
|
cookies: context.cookies,
|
|
3399
|
-
url
|
|
5087
|
+
url,
|
|
5088
|
+
lastContentWriteAt
|
|
3400
5089
|
});
|
|
3401
5090
|
const renderAndFinalize = async () => {
|
|
3402
5091
|
const t0 = performance.now();
|
|
@@ -3415,15 +5104,18 @@ const onRequest = defineMiddleware(async (context, next) => {
|
|
|
3415
5104
|
return wrapBodyForStreamMetrics(finalizeResponse(response, timings));
|
|
3416
5105
|
};
|
|
3417
5106
|
if (scoped) {
|
|
5107
|
+
const { deferredTasks, lifecycle } = coordinateScopedDbLifecycle(scoped);
|
|
3418
5108
|
const parent = getRequestContext();
|
|
3419
5109
|
return runWithContext(parent ? {
|
|
3420
5110
|
...parent,
|
|
3421
|
-
db: scoped.db
|
|
5111
|
+
db: scoped.db,
|
|
5112
|
+
deferredTasks
|
|
3422
5113
|
} : {
|
|
3423
5114
|
editMode: false,
|
|
3424
5115
|
db: scoped.db,
|
|
3425
|
-
metrics
|
|
3426
|
-
|
|
5116
|
+
metrics,
|
|
5117
|
+
deferredTasks
|
|
5118
|
+
}, () => finishScoped(lifecycle, renderAndFinalize));
|
|
3427
5119
|
}
|
|
3428
5120
|
return renderAndFinalize();
|
|
3429
5121
|
};
|
|
@@ -3456,5 +5148,5 @@ const onRequest = defineMiddleware(async (context, next) => {
|
|
|
3456
5148
|
});
|
|
3457
5149
|
|
|
3458
5150
|
//#endregion
|
|
3459
|
-
export { onRequest as default, onRequest, runScheduledTasks, withEmDashRuntime };
|
|
5151
|
+
export { onRequest as default, onRequest, runScheduledMediaUsageTasks, runScheduledTasks, withEmDashRuntime };
|
|
3460
5152
|
//# sourceMappingURL=middleware.mjs.map
|