@rebasepro/server 0.24.0 → 0.24.1-canary.gcb918fc
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/README.md +27 -2
- package/dist/GCSStorageController-fv6JAZ87.js +402 -0
- package/dist/GCSStorageController-fv6JAZ87.js.map +1 -0
- package/dist/S3StorageController-BNg-pXiY.js +480 -0
- package/dist/S3StorageController-BNg-pXiY.js.map +1 -0
- package/dist/api/mount-path.d.ts +30 -0
- package/dist/api/mount.d.ts +6 -13
- package/dist/api/rest/api-generator.d.ts +53 -1
- package/dist/api/rest/auth-collection-writes.d.ts +28 -21
- package/dist/api/rest/field-access-query.d.ts +28 -8
- package/dist/api/rest/field-ops.d.ts +19 -7
- package/dist/api/rest/query-body.d.ts +10 -0
- package/dist/api/rest/write-validation.d.ts +7 -5
- package/dist/api/schema-editor-routes.d.ts +10 -1
- package/dist/api/sql-export-routes.d.ts +8 -0
- package/dist/api/types.d.ts +35 -0
- package/dist/{ast-schema-editor-CWqS_sLJ.js → ast-schema-editor-mCeSC03x.js} +3 -3
- package/dist/{ast-schema-editor-CWqS_sLJ.js.map → ast-schema-editor-mCeSC03x.js.map} +1 -1
- package/dist/auth/access.d.ts +57 -8
- package/dist/auth/admin-user-ops.d.ts +24 -2
- package/dist/auth/admin-users-route.d.ts +1 -0
- package/dist/auth/api-keys/api-key-middleware.d.ts +10 -3
- package/dist/auth/api-keys/api-key-routes.d.ts +4 -1
- package/dist/auth/builtin-auth-adapter.d.ts +13 -0
- package/dist/auth/email-change-routes.d.ts +3 -0
- package/dist/auth/expired-token-sweep.d.ts +5 -1
- package/dist/auth/interfaces.d.ts +10 -0
- package/dist/auth/jwks-routes.d.ts +5 -1
- package/dist/auth/jwt-crypto.d.ts +7 -0
- package/dist/auth/jwt-keys.d.ts +4 -0
- package/dist/auth/jwt.d.ts +282 -166
- package/dist/auth/magic-link-routes.d.ts +3 -0
- package/dist/auth/mfa-crypto.d.ts +9 -3
- package/dist/auth/mfa-gate.d.ts +4 -2
- package/dist/auth/mfa-routes.d.ts +6 -0
- package/dist/auth/middleware.d.ts +9 -2
- package/dist/auth/otp-routes.d.ts +3 -0
- package/dist/auth/rate-limiter.d.ts +61 -34
- package/dist/auth/registration-policy.d.ts +17 -0
- package/dist/auth/require-auth.d.ts +1 -1
- package/dist/auth/routes.d.ts +16 -0
- package/dist/auth/session-routes.d.ts +6 -0
- package/dist/auth/user-picking.d.ts +37 -0
- package/dist/auth/verify-credential.d.ts +25 -4
- package/dist/{auth-DMLngxn_.js → auth-GnONzteu.js} +2869 -1871
- package/dist/auth-GnONzteu.js.map +1 -0
- package/dist/auth_adapter-CIItCTc8.js +21 -0
- package/dist/auth_adapter-CIItCTc8.js.map +1 -0
- package/dist/backend/create-backend.d.ts +281 -0
- package/dist/backend/default-backend.d.ts +35 -0
- package/dist/backend/options.d.ts +912 -0
- package/dist/backend/scope.d.ts +23 -0
- package/dist/backend/services.d.ts +54 -0
- package/dist/backend-DTAOsLQc.js.map +1 -1
- package/dist/backup/backup-common.d.ts +14 -3
- package/dist/backup/backup-routes.d.ts +23 -2
- package/dist/backup/backup-storage.d.ts +3 -1
- package/dist/backup/index.d.ts +3 -1
- package/dist/{backup-CN0s50D2.js → backup-BnvkSLHi.js} +176 -35
- package/dist/backup-BnvkSLHi.js.map +1 -0
- package/dist/boot/boot.d.ts +2 -2
- package/dist/boot/driver.d.ts +3 -0
- package/dist/boot/env.d.ts +9 -2
- package/dist/boot/http-server.d.ts +22 -0
- package/dist/boot/options.d.ts +16 -1
- package/dist/boot/provision.d.ts +4 -2
- package/dist/boot/runtime-cors.d.ts +17 -0
- package/dist/boot/sources.d.ts +2 -0
- package/dist/boot/static-routing.d.ts +9 -0
- package/dist/{contract-routes-fz8i4pxs.js → contract-routes-Ey6ftnKH.js} +4 -20
- package/dist/contract-routes-Ey6ftnKH.js.map +1 -0
- package/dist/cron/cron-loader.d.ts +77 -18
- package/dist/cron/cron-routes.d.ts +1 -1
- package/dist/cron/cron-scheduler.d.ts +69 -3
- package/dist/cron/cron-store.d.ts +33 -5
- package/dist/cron/define-cron.d.ts +6 -2
- package/dist/cron/index.d.ts +3 -3
- package/dist/cron-loader-B-u0snqq.js +202 -0
- package/dist/cron-loader-B-u0snqq.js.map +1 -0
- package/dist/{cron-routes-Bc-SB0Se.js → cron-routes-OD6BstUj.js} +27 -4
- package/dist/cron-routes-OD6BstUj.js.map +1 -0
- package/dist/{cron-scheduler-CYQgco86.js → cron-scheduler-idtLPoN-.js} +299 -33
- package/dist/cron-scheduler-idtLPoN-.js.map +1 -0
- package/dist/cron-store-CZ30ZBi-.js +577 -0
- package/dist/cron-store-CZ30ZBi-.js.map +1 -0
- package/dist/email/smtp-email-service.d.ts +23 -0
- package/dist/email/types.d.ts +18 -0
- package/dist/env.d.ts +0 -37
- package/dist/filter-dialect-O1HK6Nvr.js +1628 -0
- package/dist/filter-dialect-O1HK6Nvr.js.map +1 -0
- package/dist/{function-loader-D7o5Epjj.js → function-loader-iKhOZatS.js} +68 -38
- package/dist/function-loader-iKhOZatS.js.map +1 -0
- package/dist/{function-routes-CaNG4waN.js → function-routes-j9TwpvQe.js} +5 -4
- package/dist/function-routes-j9TwpvQe.js.map +1 -0
- package/dist/functions/context.d.ts +9 -0
- package/dist/functions/define-function.d.ts +10 -6
- package/dist/functions/function-loader.d.ts +75 -18
- package/dist/functions/function-routes.d.ts +2 -1
- package/dist/functions/index.d.ts +5 -4
- package/dist/functions/index.js +42 -13
- package/dist/functions/index.js.map +1 -1
- package/dist/functions/internal.d.ts +2 -2
- package/dist/functions/proxy.d.ts +6 -2
- package/dist/functions/request-timeout.d.ts +7 -5
- package/dist/history/history-routes.d.ts +9 -0
- package/dist/history/retention-sweep.d.ts +81 -0
- package/dist/{history-recorder-Nr8zLvoU.js → history-recorder-BfWRwmPy.js} +3 -3
- package/dist/{history-recorder-Nr8zLvoU.js.map → history-recorder-BfWRwmPy.js.map} +1 -1
- package/dist/{history-store-rcAm_xFR.js → history-store-DPofGVDp.js} +2 -2
- package/dist/{history-store-rcAm_xFR.js.map → history-store-DPofGVDp.js.map} +1 -1
- package/dist/index.d.ts +23 -10
- package/dist/index.es.js +24423 -20038
- package/dist/index.es.js.map +1 -1
- package/dist/init/docs.d.ts +27 -10
- package/dist/init/process-safety.d.ts +7 -2
- package/dist/init/shutdown.d.ts +18 -3
- package/dist/init.d.ts +49 -865
- package/dist/internal-tables-L2t39RdT.js +136 -0
- package/dist/internal-tables-L2t39RdT.js.map +1 -0
- package/dist/jobs/job-queue.d.ts +8 -0
- package/dist/jobs/job-store.d.ts +24 -0
- package/dist/jobs/types.d.ts +25 -4
- package/dist/{jobs-DqYNfquG.js → jobs-BCbflZj2.js} +234 -24
- package/dist/jobs-BCbflZj2.js.map +1 -0
- package/dist/{jwt-R6bSPMjk.js → jwt-BlvuKoR6.js} +529 -454
- package/dist/jwt-BlvuKoR6.js.map +1 -0
- package/dist/{keys-GAVZqbqx.js → keys-COzQ2aE8.js} +2 -2
- package/dist/{keys-GAVZqbqx.js.map → keys-COzQ2aE8.js.map} +1 -1
- package/dist/load-failure-L5hvfDoG.js +80 -0
- package/dist/load-failure-L5hvfDoG.js.map +1 -0
- package/dist/mcp/mcp-routes.d.ts +6 -0
- package/dist/mcp/mcp-tools.d.ts +8 -0
- package/dist/mcp/oauth-routes.d.ts +7 -0
- package/dist/metrics/index.d.ts +18 -0
- package/dist/metrics/pool-wait.d.ts +50 -0
- package/dist/offline-QNQgsPOq-B2yGeXkn.js +3305 -0
- package/dist/offline-QNQgsPOq-B2yGeXkn.js.map +1 -0
- package/dist/offline-store-Cm5sAX81-CALiO4Gn.js +1915 -0
- package/dist/offline-store-Cm5sAX81-CALiO4Gn.js.map +1 -0
- package/dist/{openapi-generator-DAq_XVDu.js → openapi-generator-D9zUQmEp.js} +366 -82
- package/dist/openapi-generator-D9zUQmEp.js.map +1 -0
- package/dist/policy-PdzX-Opd.js +121 -0
- package/dist/policy-PdzX-Opd.js.map +1 -0
- package/dist/project_manifest-R6y_KHms.js +59 -0
- package/dist/project_manifest-R6y_KHms.js.map +1 -0
- package/dist/{proxy-qRlqeUmO.js → proxy-CkTlMGmh.js} +4 -3
- package/dist/proxy-CkTlMGmh.js.map +1 -0
- package/dist/{query-parser-BgiKJKvc.js → query-parser-4WkMoA_K.js} +510 -15
- package/dist/query-parser-4WkMoA_K.js.map +1 -0
- package/dist/{request-timeout-DgH7j8qO.js → request-timeout-Dnl2_OBc.js} +15 -8
- package/dist/request-timeout-Dnl2_OBc.js.map +1 -0
- package/dist/resumable-upload-threshold-CKv-Ry0l.js +24 -0
- package/dist/resumable-upload-threshold-CKv-Ry0l.js.map +1 -0
- package/dist/schema-edit/apply-schema-change.d.ts +10 -0
- package/dist/schema-edit/local-source.d.ts +29 -0
- package/dist/schema-edit/remote-source.d.ts +13 -3
- package/dist/schema-edit/source-editor.d.ts +28 -0
- package/dist/{schema-editor-routes-oIyuWl3L.js → schema-editor-routes-C11Tumyd.js} +54 -10
- package/dist/schema-editor-routes-C11Tumyd.js.map +1 -0
- package/dist/services/outbound-post.d.ts +50 -0
- package/dist/services/outbound-url-guard.d.ts +34 -5
- package/dist/services/routed-realtime-service.d.ts +5 -0
- package/dist/services/webhook-service.d.ts +66 -14
- package/dist/singleton.d.ts +9 -5
- package/dist/soft-delete-params-DfV3vVN5.js +668 -0
- package/dist/soft-delete-params-DfV3vVN5.js.map +1 -0
- package/dist/sql-export-routes-ClRwzIsC.js +240 -0
- package/dist/sql-export-routes-ClRwzIsC.js.map +1 -0
- package/dist/{src-CatHFUym.js → src-D5Y3N-S4.js} +2 -1
- package/dist/{src-CatHFUym.js.map → src-D5Y3N-S4.js.map} +1 -1
- package/dist/storage/GCSStorageController.d.ts +46 -2
- package/dist/storage/LocalStorageController.d.ts +67 -2
- package/dist/storage/S3StorageController.d.ts +37 -3
- package/dist/storage/image-transform.d.ts +5 -0
- package/dist/storage/maintenance-routes.d.ts +23 -0
- package/dist/storage/maintenance.d.ts +126 -0
- package/dist/storage/rendition-cache.d.ts +9 -11
- package/dist/storage/request-principals.d.ts +30 -0
- package/dist/storage/routes.d.ts +20 -0
- package/dist/storage/source-variables.d.ts +10 -0
- package/dist/storage/streams.d.ts +42 -0
- package/dist/storage/tus-handler.d.ts +46 -2
- package/dist/storage/types.d.ts +82 -7
- package/dist/storage/upload-validation.d.ts +13 -0
- package/dist/storage-resumable-VO-cMpse-C_YX71pO.js +125 -0
- package/dist/storage-resumable-VO-cMpse-C_YX71pO.js.map +1 -0
- package/dist/strings.es-BeZWAodn.js +256 -0
- package/dist/strings.es-BeZWAodn.js.map +1 -0
- package/dist/upload-validation-Dd-fnsbA.js +172 -0
- package/dist/upload-validation-Dd-fnsbA.js.map +1 -0
- package/dist/utils/byte-size.d.ts +10 -0
- package/dist/utils/compression.d.ts +6 -0
- package/dist/utils/load-failure.d.ts +14 -0
- package/package.json +5 -5
- package/dist/GCSStorageController-BSiP1c-f.js +0 -256
- package/dist/GCSStorageController-BSiP1c-f.js.map +0 -1
- package/dist/S3StorageController-CAwFRgjV.js +0 -276
- package/dist/S3StorageController-CAwFRgjV.js.map +0 -1
- package/dist/auth-DMLngxn_.js.map +0 -1
- package/dist/backup-CN0s50D2.js.map +0 -1
- package/dist/contract-routes-fz8i4pxs.js.map +0 -1
- package/dist/cron-loader-CwaANlOG.js +0 -118
- package/dist/cron-loader-CwaANlOG.js.map +0 -1
- package/dist/cron-routes-Bc-SB0Se.js.map +0 -1
- package/dist/cron-scheduler-CYQgco86.js.map +0 -1
- package/dist/cron-store-D2Q9-Aco.js +0 -300
- package/dist/cron-store-D2Q9-Aco.js.map +0 -1
- package/dist/dynamic-import-X40pTZUQ.js +0 -23
- package/dist/dynamic-import-X40pTZUQ.js.map +0 -1
- package/dist/function-loader-D7o5Epjj.js.map +0 -1
- package/dist/function-routes-CaNG4waN.js.map +0 -1
- package/dist/jobs-DqYNfquG.js.map +0 -1
- package/dist/jwt-R6bSPMjk.js.map +0 -1
- package/dist/openapi-generator-DAq_XVDu.js.map +0 -1
- package/dist/proxy-qRlqeUmO.js.map +0 -1
- package/dist/query-parser-BgiKJKvc.js.map +0 -1
- package/dist/request-timeout-DgH7j8qO.js.map +0 -1
- package/dist/schema-editor-routes-oIyuWl3L.js.map +0 -1
- package/dist/soft-delete-params-BWPilMPF.js +0 -59
- package/dist/soft-delete-params-BWPilMPF.js.map +0 -1
- package/dist/src-I3aG1PcY.js +0 -6870
- package/dist/src-I3aG1PcY.js.map +0 -1
- package/dist/types-BfKcm9do.js +0 -39
- package/dist/types-BfKcm9do.js.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contract-routes-Ey6ftnKH.js","names":[],"sources":["../../types/src/types/collection_contract.ts","../../types/src/types/schema_version.ts","../src/api/contract-routes.ts"],"sourcesContent":["import type { CollectionConfig } from \"./collections\";\n\n/**\n * Serializing collections so they survive a network hop.\n *\n * A collection definition is not plain data. Relations point at their target\n * with a *function* (`target: () => usersCollection`) so two collections can\n * reference each other without an import cycle, and collections also carry\n * callbacks, custom views and component references. `JSON.stringify` silently\n * drops every one of those, which matters because the SDK generator *calls*\n * `relation.target()` to decide whether a foreign key is a string or a number.\n * Serialize naively and remote SDK generation produces subtly wrong types\n * instead of failing — the worst possible outcome.\n *\n * So relation targets are resolved to a slug reference on the way out and\n * rebuilt into functions on the way in. Everything else that cannot cross a wire\n * is dropped deliberately: an SDK is generated from the *shape* of the data, and\n * server-side behaviour is neither useful to a client nor safe to publish.\n */\n\n/** Marker replacing a relation's `target` function in serialized form. */\nexport interface SerializedCollectionRef {\n __collectionRef: string;\n}\n\nexport function isSerializedCollectionRef(value: unknown): value is SerializedCollectionRef {\n return typeof value === \"object\"\n && value !== null\n && typeof (value as SerializedCollectionRef).__collectionRef === \"string\";\n}\n\n/** Depth limit for the walk — deep enough for real configs, finite for cyclic ones. */\nconst MAX_DEPTH = 64;\n\n/**\n * Resolve whatever a `target` thunk returns down to a collection.\n *\n * A target may be the collection, a module namespace (when the authoring file\n * used `import * as`), or a default-export wrapper. All three appear in real\n * projects, and the SDK generator already unwraps them the same way.\n */\nfunction unwrapTarget(value: unknown): CollectionConfig | undefined {\n if (!value || typeof value !== \"object\") return undefined;\n const candidate = value as { default?: unknown; __esModule?: boolean; properties?: unknown };\n if (candidate.default || candidate.__esModule) {\n const inner = candidate.default;\n if (inner && typeof inner === \"object\") return inner as CollectionConfig;\n }\n if (candidate.properties) return value as CollectionConfig;\n return undefined;\n}\n\n/** The identity a serialized reference uses. Slug first — it is the routing key. */\nfunction refFor(collection: CollectionConfig | undefined): string | undefined {\n if (!collection) return undefined;\n const withPath = collection as CollectionConfig & { path?: string };\n return collection.slug || withPath.path || collection.name;\n}\n\n/**\n * Deep-copy a value into something JSON can carry.\n *\n * `target` keys are special-cased into refs. Other functions vanish, cycles are\n * cut, and everything else is copied structurally.\n */\n/** Shared walk state: the memo, plus a count of depth-cap hits. */\ninterface WalkState {\n memo: WeakMap<object, unknown>;\n /**\n * How many times the walk has truncated a subtree — by hitting the depth\n * cap, or by cutting a cycle.\n *\n * Either kind of truncation makes a result valid only at the *position* it\n * was produced at, so caching it and serving it elsewhere silently drops\n * content that would have been included. Comparing this counter before and\n * after a node's children tells us whether its result is position-\n * independent and therefore safe to memoize.\n *\n * The cycle case is the subtle one: with `a.b = b` and `b.a = a`, serializing\n * `{ first: b, second: a }` visits `a` beneath `b` — where the cycle back to\n * `b` is cut — and would then reuse that truncated `a` for `second`, where\n * nothing needed cutting.\n */\n truncations: number;\n}\n\nfunction toSerializable(\n value: unknown,\n seen: WeakSet<object>,\n depth: number,\n state: WalkState,\n key?: string\n): unknown {\n if (depth > MAX_DEPTH) {\n state.truncations++;\n return undefined;\n }\n\n if (typeof value === \"function\") {\n // Only a relation target carries information a client needs. Calling it\n // is safe here — this runs on the server, where the target module is\n // already loaded — and a throwing target simply yields no reference,\n // which degrades the generated FK type rather than failing the request.\n if (key === \"target\") {\n try {\n const resolved = unwrapTarget((value as () => unknown)());\n const ref = refFor(resolved);\n return ref ? { __collectionRef: ref } : undefined;\n } catch {\n return undefined;\n }\n }\n return undefined;\n }\n\n if (value === null || typeof value !== \"object\") {\n return value;\n }\n\n if (value instanceof Date) return value.toISOString();\n if (value instanceof RegExp) return value.source;\n\n if (seen.has(value as object)) {\n state.truncations++;\n return undefined;\n }\n\n // A shared (non-cyclic) subgraph is reachable by many paths, and `seen` is a\n // *path* set — released in the `finally` below so a node referenced twice in\n // different branches is emitted twice rather than dropped as a false cycle.\n // Without memoization that makes the walk exponential in depth: a diamond\n // graph 20 levels deep took ~400ms, and each further level doubled it. The\n // result is a plain data tree, so handing back the same converted object for\n // a repeat visit is indistinguishable after JSON.stringify.\n const cached = state.memo.get(value as object);\n if (cached !== undefined) return cached;\n\n seen.add(value as object);\n const truncationsBefore = state.truncations;\n const memoize = (result: unknown): unknown => {\n // Only cache a result that nothing was cut from.\n if (result !== undefined && state.truncations === truncationsBefore) {\n state.memo.set(value as object, result);\n }\n return result;\n };\n\n try {\n if (Array.isArray(value)) {\n const items = value\n .map(item => toSerializable(item, seen, depth + 1, state))\n .filter(item => item !== undefined);\n // A container that had content, none of which can be represented, is\n // itself unrepresentable — see the note below.\n return memoize(value.length > 0 && items.length === 0 ? undefined : items);\n }\n\n // A React element or component reference has no meaning to a client and\n // will not survive JSON anyway.\n if (\"$$typeof\" in (value as Record<string, unknown>)) return undefined;\n\n const entries = Object.entries(value as Record<string, unknown>);\n const out: Record<string, unknown> = {};\n for (const [k, v] of entries) {\n const converted = toSerializable(v, seen, depth + 1, state, k);\n if (converted !== undefined) out[k] = converted;\n }\n\n // Drop a container whose entire content was dropped.\n //\n // `callbacks: { beforeSave() {…} }` would otherwise serialize to\n // `callbacks: {}` — an empty husk that carries no information but is not\n // *nothing*, so it lands in the payload and, worse, in the schema hash.\n // Editing a hook would then change every client's schema version and\n // report perfectly current SDKs as stale.\n //\n // A container that started empty stays empty: `properties: {}` is a\n // deliberate statement, not a casualty.\n if (entries.length > 0 && Object.keys(out).length === 0) return undefined;\n\n return memoize(out);\n } finally {\n // Released so a collection referenced twice in different branches is\n // emitted twice rather than being dropped as a false cycle.\n seen.delete(value as object);\n }\n}\n\n/**\n * Serialize collections for transport over the contract endpoint.\n *\n * Sorted by slug so the output — and therefore the schema hash computed from it\n * — does not depend on filesystem ordering.\n */\nexport function serializeCollections(collections: CollectionConfig[]): unknown[] {\n return [...collections]\n .sort((a, b) => String(a.slug ?? \"\").localeCompare(String(b.slug ?? \"\")))\n .map(collection => toSerializable(withoutAdminBlock(collection), new WeakSet(), 0, {\n memo: new WeakMap(),\n truncations: 0\n }))\n .filter((c): c is Record<string, unknown> => c !== undefined);\n}\n\n/**\n * Drop the admin block before the walk.\n *\n * Nothing downstream of serialization is an admin panel. The contract endpoint\n * feeds remote SDK generation, and `rebase build` writes the result into a bundle\n * manifest that only the backend runtime reads. The block would survive the walk\n * as a husk anyway — its React elements and component functions are dropped\n * individually — and that husk has two costs worth avoiding: it puts every custom\n * component's *file path* on an endpoint whose job is to describe data shapes, and\n * it grows a payload that is fetched and cached per project.\n *\n * Removing it here rather than at each call site means one chokepoint, so a future\n * consumer of `serializeCollections` cannot forget.\n *\n * Child collections carry their own block, so this recurses — stripping only the\n * top level was the mistake `stripNonClientFields` in the contract routes already\n * had to fix once for security rules.\n */\nfunction withoutAdminBlock(collection: CollectionConfig): CollectionConfig {\n const { admin: _admin, ...rest } = collection as CollectionConfig & Record<string, unknown>;\n const nested = rest as Record<string, unknown>;\n if (Array.isArray(nested.subcollections)) {\n nested.subcollections = nested.subcollections.map(\n (child) => withoutAdminBlock(child as CollectionConfig)\n );\n }\n return rest as CollectionConfig;\n}\n\n/**\n * Rebuild collections received from a contract endpoint.\n *\n * Relation refs become real thunks resolving through the returned set, so\n * downstream consumers — the SDK generator above all — see exactly the shape\n * they would have seen had the collections been imported from source.\n *\n * A ref naming a collection that is not in the payload resolves to `undefined`\n * rather than throwing: the generator already tolerates an unresolvable target\n * by falling back to a permissive key type, and a partial contract should still\n * produce a usable SDK.\n */\nexport function deserializeCollections(payload: unknown[]): CollectionConfig[] {\n const collections = payload\n .filter((c): c is Record<string, unknown> => typeof c === \"object\" && c !== null)\n .map(c => ({ ...c })) as unknown as CollectionConfig[];\n\n const bySlug = new Map<string, CollectionConfig>();\n for (const collection of collections) {\n const ref = refFor(collection);\n if (ref) bySlug.set(ref, collection);\n }\n\n const rehydrate = (value: unknown, depth: number): void => {\n if (depth > MAX_DEPTH || !value || typeof value !== \"object\") return;\n\n if (Array.isArray(value)) {\n for (const item of value) rehydrate(item, depth + 1);\n return;\n }\n\n const record = value as Record<string, unknown>;\n for (const [key, child] of Object.entries(record)) {\n if (key === \"target\" && isSerializedCollectionRef(child)) {\n const slug = child.__collectionRef;\n record.target = () => bySlug.get(slug);\n continue;\n }\n rehydrate(child, depth + 1);\n }\n };\n\n for (const collection of collections) rehydrate(collection, 0);\n return collections;\n}\n","import type { CollectionConfig } from \"./collections\";\nimport { serializeCollections } from \"./collection_contract\";\n\n/**\n * The schema version stamp.\n *\n * One function, used in three places that must agree or the whole drift-detection\n * story is noise: `rebase build` writes it into a bundle manifest, the runtime\n * serves it from the contract endpoint, and a generated SDK records the value it\n * was built from. If any two of those computed it differently, every client would\n * look permanently out of date.\n *\n * It covers **collections only** — the client's contract is the shape of the\n * data, so editing a hook or a server function must not invalidate every SDK in\n * every repository. That is a deliberate narrowing, not an oversight.\n */\n\n/** Stable stringify: object keys sorted at every level, so key order cannot alter the hash. */\nfunction canonicalize(value: unknown): string {\n if (value === null || typeof value !== \"object\") {\n return JSON.stringify(value) ?? \"null\";\n }\n if (Array.isArray(value)) {\n return `[${value.map(canonicalize).join(\",\")}]`;\n }\n const entries = Object.entries(value as Record<string, unknown>)\n .filter(([, v]) => v !== undefined)\n .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));\n return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${canonicalize(v)}`).join(\",\")}}`;\n}\n\n/**\n * Reduce a collection to the parts a generated client is actually built from.\n *\n * The version answers one question — \"is this SDK stale?\" — so it must change\n * exactly when the generated types could change, and never otherwise. Hashing a\n * whole collection fails both halves of that:\n *\n * - Security rules, callbacks, icons, groups and UI settings do not appear in a\n * generated client, so including them reports perfectly current SDKs as stale.\n * - Worse, they are not stable *inputs*. The runtime applies default security\n * rules when it loads collections, so the same source hashed before and after\n * loading produced two different answers — a build-time stamp that could never\n * match the server that served it.\n *\n * Codegen reads the slug (for the `Database` key and type names), the properties,\n * and the relations. That is the projection.\n */\nfunction projectForCodegen(collection: CollectionConfig): Record<string, unknown> {\n const source = collection as CollectionConfig & {\n relations?: unknown;\n subcollections?: CollectionConfig[];\n path?: string;\n engine?: unknown;\n dataSource?: unknown;\n };\n\n return {\n slug: collection.slug ?? source.path,\n properties: collection.properties,\n relations: source.relations,\n // The engine decides whether relations are resolved at all: codegen asks\n // `getDataSourceCapabilities(collection.engine).supportsRelations`, and an\n // engine that answers no drops every foreign-key column from the\n // generated Row/Insert/Update types. Moving a collection to such an\n // engine is a real change to the generated types, so it has to move the\n // version. `dataSource` is what resolves to `engine`, so it counts too.\n engine: source.engine,\n dataSource: source.dataSource,\n subcollections: source.subcollections?.map(projectForCodegen)\n };\n}\n\n/**\n * Compute the canonical string a schema version hashes.\n *\n * Exposed separately so the hashing itself can differ by environment: Node has\n * `crypto`, and callers without it can still compare canonical forms directly.\n */\nexport function canonicalSchemaPayload(collections: CollectionConfig[]): string {\n const projected = serializeCollections(collections)\n .map(collection => projectForCodegen(collection as CollectionConfig));\n return canonicalize(projected);\n}\n\n/**\n * A short, non-cryptographic digest of the canonical payload.\n *\n * FNV-1a style, 64 bits, as two 32-bit halves. This is an identity, not a\n * security boundary: nothing trusts a schema version to prove anything, it only\n * answers \"is this the same schema as before\". A hand-rolled hash keeps this\n * module free of `node:crypto`, so the identical function runs in the browser,\n * in the CLI, and in the runtime — which is the property that actually matters.\n */\nexport function computeSchemaVersion(collections: CollectionConfig[]): string {\n const payload = canonicalSchemaPayload(collections);\n\n let h1 = 0x811c9dc5;\n let h2 = 0x01000193;\n\n for (let i = 0; i < payload.length; i++) {\n const code = payload.charCodeAt(i);\n h1 ^= code;\n // Multiply by the FNV prime using shifts to stay in 32-bit integer math.\n h1 = (h1 + ((h1 << 1) + (h1 << 4) + (h1 << 7) + (h1 << 8) + (h1 << 24))) >>> 0;\n h2 ^= code + i;\n h2 = (h2 + ((h2 << 1) + (h2 << 5) + (h2 << 9) + (h2 << 15) + (h2 << 24))) >>> 0;\n }\n\n const hex = (n: number): string => n.toString(16).padStart(8, \"0\");\n return `v1:${hex(h1)}${hex(h2)}`;\n}\n","import { Hono } from \"hono\";\nimport {\n RUNTIME_CONTRACT_VERSION,\n SCHEMA_VERSION_HEADER,\n computeSchemaVersion,\n serializeCollections,\n type CollectionConfig,\n type RebaseProjectContract\n} from \"@rebasepro/types\";\nimport type { HonoEnv } from \"./types\";\nimport { logger } from \"../utils/logger\";\n\n/**\n * The project contract endpoint.\n *\n * This is what makes a repository able to build against a project it does not\n * contain. Without it, a typed client can only be generated from local\n * collection *source*, which means every frontend must live in the same\n * repository as the backend. Serving the contract turns that around: an app\n * asks the project what its shape is, so a web app, a second web app and a\n * mobile app can each live wherever they like and none of them needs to know\n * about the others.\n *\n * Admin-gated. Collection definitions describe every table, column and relation\n * in the project, including ones no security rule would ever expose — that is a\n * map of the database, not public API documentation.\n */\n\nexport interface ContractRoutesConfig {\n collectionRegistry: { getRawCollections(): CollectionConfig[] };\n /**\n * The schema version recorded at build time.\n *\n * Preferred over recomputing, so that what a client is told matches exactly\n * what the bundle claims. It is recomputed only when a bundle did not record\n * one — a `baas`-mode project derives its collections from the live database\n * at boot, so there was nothing to hash when it was built.\n */\n schemaVersion?: string;\n /** Runtime package version, surfaced so a client can report what it built against. */\n runtimeVersion?: string;\n}\n\n/**\n * Strip everything a client does not need from a serialized collection.\n *\n * The generator reads the slug, the properties and the relations. It never reads\n * a security rule — but `securityRules` carries the raw SQL of every RLS\n * predicate guarding the project, which is a description of the authorization\n * model rather than of the data shape. Publishing it to anyone who can generate\n * an SDK gives away more than the endpoint is for, so it is removed here rather\n * than trusted not to matter.\n */\nfunction stripNonClientFields(collection: unknown): unknown {\n if (!collection || typeof collection !== \"object\") return collection;\n const {\n securityRules: _securityRules,\n callbacks: _callbacks,\n ...rest\n } = collection as Record<string, unknown>;\n\n // Subcollections are collections, and carry their own rules. Stripping only\n // the top level published every nested policy — the leak this exists to\n // prevent, just one level down.\n if (Array.isArray(rest.subcollections)) {\n rest.subcollections = rest.subcollections.map(stripNonClientFields);\n }\n\n return rest;\n}\n\nexport function createContractRoutes(config: ContractRoutesConfig): Hono<HonoEnv> {\n const router = new Hono<HonoEnv>();\n\n // Computing a version walks and canonicalizes every collection, and\n // `/schema-version` is deliberately unauthenticated and meant to be polled.\n // Recomputing per request would make a CI convenience into a CPU\n // amplification anyone could aim at the server. Collections do not change\n // after boot, so once is enough.\n let cachedVersion: string | undefined;\n const schemaVersionOf = (collections: CollectionConfig[]): string => {\n if (config.schemaVersion) return config.schemaVersion;\n if (cachedVersion === undefined) cachedVersion = computeSchemaVersion(collections);\n return cachedVersion;\n };\n\n router.get(\"/contract\", (c) => {\n const collections = config.collectionRegistry.getRawCollections();\n const serialized = serializeCollections(collections).map(stripNonClientFields);\n\n // In `baas` mode the collections are whatever introspection found at\n // boot, so the version has to be computed from them rather than taken\n // from a build that never saw them.\n const schemaVersion = schemaVersionOf(collections);\n\n const contract: RebaseProjectContract = {\n schemaVersion,\n runtime: {\n version: config.runtimeVersion ?? \"unknown\",\n contract: RUNTIME_CONTRACT_VERSION\n },\n collections: serialized,\n collectionSlugs: collections\n .map(collection => collection.slug)\n .filter((slug): slug is string => Boolean(slug))\n .sort(),\n generatedAt: new Date().toISOString()\n };\n\n c.header(SCHEMA_VERSION_HEADER, schemaVersion);\n return c.json(contract);\n });\n\n /**\n * Cheap drift check.\n *\n * Deliberately unauthenticated and deliberately tiny: two version stamps\n * and nothing else. A CI job that only wants to know whether its generated\n * SDK is stale should not need admin credentials, and a version stamp\n * reveals nothing about the schema it stands for.\n *\n * `runtime` is here as well as on `/contract` because the two answer\n * different questions and only one of them was reachable. Which runtime a\n * project is on decides whether a client's wire format is understood at\n * all, and it was published solely on the admin-gated route — so a CLI or\n * an SDK, the two callers that actually need to know, could not ask. That\n * is the same shape as the header this route echoes: a documented signal\n * with no reachable sender. `contract` is the number that matters for\n * compatibility; `version` names the release a human should quote.\n */\n router.get(\"/schema-version\", (c) => {\n const schemaVersion = schemaVersionOf(config.collectionRegistry.getRawCollections());\n c.header(SCHEMA_VERSION_HEADER, schemaVersion);\n return c.json({\n schemaVersion,\n runtime: {\n version: config.runtimeVersion ?? \"unknown\",\n contract: RUNTIME_CONTRACT_VERSION\n }\n });\n });\n\n logger.debug(\"Contract routes mounted\");\n return router;\n}\n"],"mappings":"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"}
|
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
import { type CronJobDefinition, type CronResourceOptions } from "@rebasepro/types";
|
|
2
2
|
import { type ModuleImporter } from "../utils/dynamic-import.js";
|
|
3
|
+
import type { LoadDirectoryOptions } from "../functions/function-loader.js";
|
|
4
|
+
/**
|
|
5
|
+
* Cron jobs as values: the job id, to its definition.
|
|
6
|
+
*
|
|
7
|
+
* `crons: { "nightly-report": nightlyReport }` schedules `nightlyReport` as the
|
|
8
|
+
* job `nightly-report` — the same thing a file named `crons/nightly-report.ts`
|
|
9
|
+
* does. {@link loadCrons} returns exactly this shape, so
|
|
10
|
+
* `crons: await loadCrons(dir)` and `cronsDir: dir` schedule the same jobs.
|
|
11
|
+
*
|
|
12
|
+
* A value is what the author wrote — `defineCron({...})` — with no defaults
|
|
13
|
+
* filled in. The scheduler fills them in when it registers the job.
|
|
14
|
+
*/
|
|
15
|
+
export type CronsRecord = Record<string, CronJobDefinition>;
|
|
16
|
+
/** A job as the scheduler registers it: its id, and its definition with defaults filled in. */
|
|
3
17
|
export interface LoadedCronJob {
|
|
4
18
|
/** Job ID derived from filename (e.g. "cleanup-sessions"). */
|
|
5
19
|
id: string;
|
|
@@ -13,34 +27,79 @@ export interface LoadedCronJobs {
|
|
|
13
27
|
problems: string[];
|
|
14
28
|
}
|
|
15
29
|
/**
|
|
16
|
-
*
|
|
30
|
+
* Load the cron jobs in a directory, as values.
|
|
31
|
+
*
|
|
32
|
+
* Each file default-exports a `CronJobDefinition` — `defineCron({...})`. The
|
|
33
|
+
* filename without its extension is the record key, which is the job id:
|
|
34
|
+
* `crons/cleanup-sessions.ts` → `{ "cleanup-sessions": definition }`.
|
|
35
|
+
*
|
|
36
|
+
* Loading declares nothing: the definitions come back exactly as written, and
|
|
37
|
+
* whoever uses them decides what they are for — a backend schedules them, the
|
|
38
|
+
* build's derive step records them in the resource graph ({@link declareCrons}).
|
|
39
|
+
*
|
|
40
|
+
* Never throws for a bad file: it is skipped, logged, and passed to
|
|
41
|
+
* `options.onSkip`.
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* ```ts
|
|
45
|
+
* const crons = await loadCrons(path.resolve("crons"));
|
|
46
|
+
* const backend = await createRebaseBackend({ database, crons, provision: true });
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
export declare function loadCrons(directory: string, options?: LoadDirectoryOptions): Promise<CronsRecord>;
|
|
50
|
+
/**
|
|
51
|
+
* Record each cron in the project's resource graph — what `rebase resources`
|
|
52
|
+
* lists and a host reads to know a schedule exists, without running it.
|
|
17
53
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
54
|
+
* Keyed by the same id the scheduler, the routes and the Studio use, and built
|
|
55
|
+
* from the definitions as written: a default the scheduler fills in
|
|
56
|
+
* (`enabled: true`, `timeoutSeconds: 300`) is this process's, not something the
|
|
57
|
+
* author declared.
|
|
58
|
+
*/
|
|
59
|
+
export declare function declareCrons(crons: CronsRecord): void;
|
|
60
|
+
/**
|
|
61
|
+
* The jobs a scheduler registers, from cron values: the definitions with this
|
|
62
|
+
* process's defaults filled in.
|
|
63
|
+
*
|
|
64
|
+
* Spread first, then normalise. Rebuilding the object field by field dropped
|
|
65
|
+
* `catchUpWindowSeconds` once — and since this is the path every job takes to
|
|
66
|
+
* the scheduler, catch-up never ran for any job authored the documented way. A
|
|
67
|
+
* field added to `CronJobDefinition` tomorrow would be dropped the same way.
|
|
21
68
|
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
69
|
+
* @throws when a value has no `schedule` or no `handler`. A file like that is
|
|
70
|
+
* skipped by the loader with a reason; a value handed over in code is refused,
|
|
71
|
+
* because the caller is right there to fix it.
|
|
72
|
+
*/
|
|
73
|
+
export declare function cronJobsFrom(crons: CronsRecord): LoadedCronJob[];
|
|
74
|
+
/**
|
|
75
|
+
* @deprecated Use `loadCrons` instead, which returns the jobs as a record keyed
|
|
76
|
+
* by id. Will be removed in a future release.
|
|
25
77
|
*/
|
|
26
78
|
export declare function loadCronJobsFromDirectory(directory: string, importModule?: ModuleImporter): Promise<LoadedCronJob[]>;
|
|
27
79
|
/**
|
|
28
|
-
*
|
|
80
|
+
* @deprecated Use `loadCrons` instead, with `onSkip` to collect the files it
|
|
81
|
+
* skipped — and `declareCrons` where the jobs belong in the resource graph,
|
|
82
|
+
* which this used to do as a side effect of loading. Will be removed in a
|
|
83
|
+
* future release.
|
|
84
|
+
*/
|
|
85
|
+
export declare function loadCronJobsWithDiagnostics(directory: string, importModule?: ModuleImporter): Promise<LoadedCronJobs>;
|
|
86
|
+
/**
|
|
87
|
+
* Read a directory of cron files: the definitions that loaded, keyed by id, and
|
|
88
|
+
* one `<file> (<reason>)` per file that will NOT be scheduled.
|
|
29
89
|
*
|
|
30
|
-
* A job that does not load does not run, and
|
|
90
|
+
* A job that does not load does not run, and the only trace used to be one
|
|
31
91
|
* `warn` per file in the boot log — next to nothing at 3am when the report that
|
|
32
|
-
* should have been mailed was not.
|
|
33
|
-
*
|
|
34
|
-
* and a log line is not a surface.
|
|
92
|
+
* should have been mailed was not. So the skips are counted, summarised in one
|
|
93
|
+
* place, and answered by `GET /api/admin/cron`.
|
|
35
94
|
*
|
|
36
|
-
* Never throws, for the same reason
|
|
95
|
+
* Never throws, for the same reason the function loader does not: one
|
|
37
96
|
* malformed file must not take the server down, and the other jobs in the
|
|
38
|
-
* directory are still worth scheduling.
|
|
39
|
-
* counted, summarised in one place, and answered by `GET /api/cron` — so the
|
|
40
|
-
* question "why did nothing run?" has an answer that does not require log
|
|
41
|
-
* access.
|
|
97
|
+
* directory are still worth scheduling.
|
|
42
98
|
*/
|
|
43
|
-
export declare function
|
|
99
|
+
export declare function readCronsDirectory(directory: string, importModule?: ModuleImporter): Promise<{
|
|
100
|
+
crons: CronsRecord;
|
|
101
|
+
problems: string[];
|
|
102
|
+
}>;
|
|
44
103
|
/**
|
|
45
104
|
* The declaration a definition makes: every field a host can read without the
|
|
46
105
|
* handler. Spelled once here so the graph and the scheduler cannot disagree
|
|
@@ -7,7 +7,7 @@ import { type CronScheduler } from "./cron-scheduler.js";
|
|
|
7
7
|
* Routes:
|
|
8
8
|
* GET / → list all cron jobs
|
|
9
9
|
* GET /:id → get a single job's status
|
|
10
|
-
* POST /:id/trigger → manually trigger a job
|
|
10
|
+
* POST /:id/trigger → manually trigger a job (`?async=1`: 202 once it has started)
|
|
11
11
|
* GET /:id/logs → get execution logs for a job
|
|
12
12
|
* PUT /:id → update job (enable/disable, or `null` to follow the code)
|
|
13
13
|
*
|
|
@@ -30,9 +30,9 @@ export declare function parseCronExpression(expression: string, after: Date, tim
|
|
|
30
30
|
* The latest slot matching `expression` within the inclusive window
|
|
31
31
|
* `[from, to]`, or `undefined` when the expression has no slot in it.
|
|
32
32
|
*
|
|
33
|
-
* Walks backwards
|
|
34
|
-
*
|
|
35
|
-
*
|
|
33
|
+
* Walks backwards from `to`, so the first hit is already the answer — in the
|
|
34
|
+
* common case (a job that ran normally moments ago) that is a handful of
|
|
35
|
+
* steps, not a scan of the whole window.
|
|
36
36
|
*
|
|
37
37
|
* `to`'s own minute is included: an instance booting at 06:00:30 has *not* run
|
|
38
38
|
* the 06:00 slot — `parseCronExpression` already skipped past it to tomorrow —
|
|
@@ -43,6 +43,29 @@ export declare function parseCronExpression(expression: string, after: Date, tim
|
|
|
43
43
|
* ISO string down either path. The claim key depends on that.
|
|
44
44
|
*/
|
|
45
45
|
export declare function findMostRecentSlot(expression: string, from: Date, to: Date, timezone?: string): Date | undefined;
|
|
46
|
+
/**
|
|
47
|
+
* What one run keeps of its `ctx.log` lines: up to 500 lines or 64 KB,
|
|
48
|
+
* whichever comes first. The lines past that are counted, and one line at the
|
|
49
|
+
* end says how many there were; every line still reaches the server log.
|
|
50
|
+
*
|
|
51
|
+
* Uncapped, a job logging one line per item kept them all — in the 50-run ring
|
|
52
|
+
* per job, in a `cron_logs` row, and in every history read: a per-minute sync
|
|
53
|
+
* of 1,000 items held 4 MB per job in memory and wrote 115 MB a day.
|
|
54
|
+
*/
|
|
55
|
+
export declare const MAX_CAPTURED_LOG_LINES = 500;
|
|
56
|
+
export declare const MAX_CAPTURED_LOG_BYTES: number;
|
|
57
|
+
/**
|
|
58
|
+
* What {@link CronScheduler.startJob} answers: the run began at `startedAt`
|
|
59
|
+
* (its history entry will carry the same timestamp), or it did not start and
|
|
60
|
+
* `log` is the skip that says why.
|
|
61
|
+
*/
|
|
62
|
+
export type StartedCronRun = {
|
|
63
|
+
started: true;
|
|
64
|
+
startedAt: string;
|
|
65
|
+
} | {
|
|
66
|
+
started: false;
|
|
67
|
+
log: CronJobLogEntry;
|
|
68
|
+
};
|
|
46
69
|
/**
|
|
47
70
|
* Whether a log entry records a run that did not happen because one was
|
|
48
71
|
* already executing — on this process, or on another that holds the job's run
|
|
@@ -69,6 +92,10 @@ export declare class CronScheduler {
|
|
|
69
92
|
* ever clear the lease that run took.
|
|
70
93
|
*/
|
|
71
94
|
private readonly instanceLabel;
|
|
95
|
+
/** The hourly claim- and log-retention sweep, while the scheduler is started with a store. */
|
|
96
|
+
private claimSweepTimer?;
|
|
97
|
+
private sweepingClaims;
|
|
98
|
+
private sweepingLogs;
|
|
72
99
|
/**
|
|
73
100
|
* Set the server singleton to make it available to cron job handlers.
|
|
74
101
|
*
|
|
@@ -98,6 +125,14 @@ export declare class CronScheduler {
|
|
|
98
125
|
* Start the scheduler — begins ticking all enabled jobs.
|
|
99
126
|
*/
|
|
100
127
|
start(): void;
|
|
128
|
+
/** Delete slot claims past retention. Failures are logged; the next hour tries again. */
|
|
129
|
+
private sweepClaims;
|
|
130
|
+
/**
|
|
131
|
+
* Delete run history past retention, and count the runs not counted yet.
|
|
132
|
+
* Failures are logged; the next hour tries again, from where this one
|
|
133
|
+
* stopped — each batch it finished is committed.
|
|
134
|
+
*/
|
|
135
|
+
private sweepLogs;
|
|
101
136
|
/**
|
|
102
137
|
* Stop the scheduler: clear every timer, then wait for the runs executing
|
|
103
138
|
* right now — for at most `timeoutMs` when given.
|
|
@@ -204,6 +239,22 @@ export declare class CronScheduler {
|
|
|
204
239
|
* recognises it.
|
|
205
240
|
*/
|
|
206
241
|
triggerJob(id: string): Promise<CronJobLogEntry | undefined>;
|
|
242
|
+
/**
|
|
243
|
+
* Start a job now and answer as soon as the run has begun, not when it
|
|
244
|
+
* ends — the manual trigger's `?async=1`.
|
|
245
|
+
*
|
|
246
|
+
* The synchronous {@link triggerJob} holds its HTTP request open for the
|
|
247
|
+
* whole run, and a proxy in front of the server (many stop at 60 s) cuts
|
|
248
|
+
* a long job's answer while the run carries on. This resolves once the
|
|
249
|
+
* run lease is held and the handler is about to be called, so a run that
|
|
250
|
+
* cannot start — already executing here or on the lease holder — is still
|
|
251
|
+
* answered as the skip it is. The run itself is tracked like any other:
|
|
252
|
+
* `stop()` waits for it, and its outcome lands in the run history, where
|
|
253
|
+
* the entry carries `manual: true` and the `startedAt` returned here.
|
|
254
|
+
*
|
|
255
|
+
* Returns `undefined` if the job doesn't exist.
|
|
256
|
+
*/
|
|
257
|
+
startJob(id: string): Promise<StartedCronRun | undefined>;
|
|
207
258
|
/**
|
|
208
259
|
* Record a run that did not happen because the previous one had not
|
|
209
260
|
* finished.
|
|
@@ -267,6 +318,21 @@ export declare class CronScheduler {
|
|
|
267
318
|
* rest as a view for {@link toStatus}. Never throws.
|
|
268
319
|
*/
|
|
269
320
|
private readFleet;
|
|
321
|
+
/**
|
|
322
|
+
* The next slot of `job` after `now`, for a listing on a process whose
|
|
323
|
+
* scheduler is not started — the `api` role, which Studio's Cron panel
|
|
324
|
+
* polls every 15 seconds.
|
|
325
|
+
*
|
|
326
|
+
* Cached on the job. The key is the job's schedule and zone (a re-register
|
|
327
|
+
* replaces the job, and with it the cache); the entry holds while
|
|
328
|
+
* `computedAt <= now < slot`. That is the same answer, not an
|
|
329
|
+
* approximation: the search found no matching minute between
|
|
330
|
+
* `computedAt`'s next minute and `slot`, so from any later instant before
|
|
331
|
+
* `slot` it lands on `slot` again. A clock that moves back, or reaching the
|
|
332
|
+
* slot, recomputes. The cache holds a pure function of the schedule, so
|
|
333
|
+
* there is nothing user- or role-scoped in it.
|
|
334
|
+
*/
|
|
335
|
+
private listedNextRun;
|
|
270
336
|
/**
|
|
271
337
|
* Warn once at start when the process looks like it is running on a
|
|
272
338
|
* platform that freezes or evicts instances between requests, where the
|
|
@@ -11,7 +11,12 @@ import type { DataDriver } from "@rebasepro/types";
|
|
|
11
11
|
* occurs, and a pause or a trigger's "already executing" is per process.
|
|
12
12
|
*/
|
|
13
13
|
export interface CronStore {
|
|
14
|
-
/**
|
|
14
|
+
/**
|
|
15
|
+
* Ensure the backing tables exist and are locked away from end users. Runs
|
|
16
|
+
* once per process per database, however many stores ask: concurrent
|
|
17
|
+
* callers share the attempt in flight, and an attempt that fell short is
|
|
18
|
+
* made again on the next call.
|
|
19
|
+
*/
|
|
15
20
|
ensureTable(): Promise<void>;
|
|
16
21
|
/** Persist a single log entry after execution. */
|
|
17
22
|
insertLog(entry: CronJobLogEntry): Promise<void>;
|
|
@@ -28,7 +33,8 @@ export interface CronStore {
|
|
|
28
33
|
fetchLogs(jobId: string, limit?: number): Promise<CronJobLogEntry[]>;
|
|
29
34
|
/**
|
|
30
35
|
* Fetch aggregate stats for all jobs (totalRuns, totalFailures, lastRunAt).
|
|
31
|
-
* Used to seed in-memory counters on startup.
|
|
36
|
+
* Used to seed in-memory counters on startup. The SQL store reads them from
|
|
37
|
+
* the all-time counters on `cron_job_state`, not from the history.
|
|
32
38
|
*/
|
|
33
39
|
fetchJobStats(): Promise<Map<string, {
|
|
34
40
|
totalRuns: number;
|
|
@@ -86,11 +92,33 @@ export interface CronStore {
|
|
|
86
92
|
/** Release a lease taken with {@link tryAcquireRunLease} — only if `holder` still holds it. */
|
|
87
93
|
releaseRunLease?(jobId: string, holder: string): Promise<void>;
|
|
88
94
|
/**
|
|
89
|
-
* Run count, failure count and last run of each job
|
|
90
|
-
*
|
|
91
|
-
*
|
|
95
|
+
* Run count, failure count and last run of each job — every process's
|
|
96
|
+
* runs, where the scheduler's own counters hold only the ones it made. The
|
|
97
|
+
* SQL store keeps them as all-time counters on `cron_job_state`, so they
|
|
98
|
+
* outlive the history's retention.
|
|
92
99
|
*/
|
|
93
100
|
fetchRunSummaries?(jobIds: readonly string[]): Promise<Map<string, CronJobRunSummary>>;
|
|
101
|
+
/**
|
|
102
|
+
* Delete slot claims past retention — all but each job's most recent one —
|
|
103
|
+
* in batches. Resolves with how many went. The scheduler runs it on a
|
|
104
|
+
* timer while it is started; a store without it keeps every claim.
|
|
105
|
+
*/
|
|
106
|
+
sweepClaims?(): Promise<number>;
|
|
107
|
+
/**
|
|
108
|
+
* Delete run history past retention, and count into the job counters any
|
|
109
|
+
* run that is not counted yet — a history written before the counters
|
|
110
|
+
* existed, or by an older server during a rolling deploy. Both in batches.
|
|
111
|
+
* The scheduler runs it with {@link sweepClaims}; a store without it keeps
|
|
112
|
+
* every run.
|
|
113
|
+
*/
|
|
114
|
+
sweepLogs?(): Promise<CronLogSweep>;
|
|
115
|
+
}
|
|
116
|
+
/** What one {@link CronStore.sweepLogs} did. */
|
|
117
|
+
export interface CronLogSweep {
|
|
118
|
+
/** History rows deleted past retention. */
|
|
119
|
+
deleted: number;
|
|
120
|
+
/** Runs counted into the job counters that were not counted before. */
|
|
121
|
+
counted: number;
|
|
94
122
|
}
|
|
95
123
|
/** One job's row in `rebase.cron_job_state`, as the scheduler reads it. */
|
|
96
124
|
export interface CronJobPersistedState {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CronJobDefinition } from "@rebasepro/types";
|
|
1
|
+
import type { CronJobDefinition, RegisteredDatabase } from "@rebasepro/types";
|
|
2
2
|
/**
|
|
3
3
|
* Typed authoring helper for a cron job file. Identity at runtime —
|
|
4
4
|
* a plain default-exported {@link CronJobDefinition} works identically;
|
|
@@ -38,5 +38,9 @@ import type { CronJobDefinition } from "@rebasepro/types";
|
|
|
38
38
|
* },
|
|
39
39
|
* });
|
|
40
40
|
* ```
|
|
41
|
+
*
|
|
42
|
+
* `ctx.rebase` is typed from the `Database` the project registered on
|
|
43
|
+
* `Register` (see the typed server data guide), and untyped when it registered
|
|
44
|
+
* none. `defineCron<Database>({…})` types one job without the registration.
|
|
41
45
|
*/
|
|
42
|
-
export declare function defineCron(definition: CronJobDefinition): CronJobDefinition
|
|
46
|
+
export declare function defineCron<DB = RegisteredDatabase>(definition: CronJobDefinition<DB>): CronJobDefinition<DB>;
|
package/dist/cron/index.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
export { loadCronJobsFromDirectory, loadCronJobsWithDiagnostics } from "./cron-loader.js";
|
|
2
|
-
export type { LoadedCronJob, LoadedCronJobs } from "./cron-loader.js";
|
|
1
|
+
export { loadCrons, declareCrons, cronJobsFrom, loadCronJobsFromDirectory, loadCronJobsWithDiagnostics } from "./cron-loader.js";
|
|
2
|
+
export type { CronsRecord, LoadedCronJob, LoadedCronJobs } from "./cron-loader.js";
|
|
3
3
|
export { defineCron } from "./define-cron.js";
|
|
4
4
|
export { CronScheduler, validateCronExpression } from "./cron-scheduler.js";
|
|
5
5
|
export { createCronRoutes } from "./cron-routes.js";
|
|
6
6
|
export { createCronStore } from "./cron-store.js";
|
|
7
7
|
export { detectFreezableRuntime, buildScaleToZeroWarning, CRON_ALWAYS_ON_ENV } from "./scale-to-zero.js";
|
|
8
|
-
export type { CronStore, CronJobPersistedState, CronRunLease, CronJobRunSummary } from "./cron-store.js";
|
|
8
|
+
export type { CronStore, CronJobPersistedState, CronRunLease, CronJobRunSummary, CronLogSweep } from "./cron-store.js";
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { createRequire as __rebaseCreateRequire } from "module";
|
|
2
|
+
import __rebaseProcess from "process";
|
|
3
|
+
globalThis.process ??= __rebaseProcess;
|
|
4
|
+
__rebaseCreateRequire(import.meta.url);
|
|
5
|
+
import { n as __exportAll } from "./rolldown-runtime-dW7B1o5h.js";
|
|
6
|
+
import { x as declareCron } from "./src-D5Y3N-S4.js";
|
|
7
|
+
import { r as logger } from "./logger-D-S-hO5e.js";
|
|
8
|
+
import { i as warnDeprecated, n as describeLoadFailure, r as nativeDynamicImport, t as SYNTAX_ERROR_ADVICE } from "./load-failure-L5hvfDoG.js";
|
|
9
|
+
import * as fs$1 from "fs";
|
|
10
|
+
import * as path$1 from "path";
|
|
11
|
+
import { pathToFileURL } from "url";
|
|
12
|
+
//#region src/cron/cron-loader.ts
|
|
13
|
+
var cron_loader_exports = /* @__PURE__ */ __exportAll({
|
|
14
|
+
cronJobsFrom: () => cronJobsFrom,
|
|
15
|
+
cronResourceOptions: () => cronResourceOptions,
|
|
16
|
+
declareCrons: () => declareCrons,
|
|
17
|
+
loadCronJobsFromDirectory: () => loadCronJobsFromDirectory,
|
|
18
|
+
loadCronJobsWithDiagnostics: () => loadCronJobsWithDiagnostics,
|
|
19
|
+
loadCrons: () => loadCrons,
|
|
20
|
+
readCronsDirectory: () => readCronsDirectory
|
|
21
|
+
});
|
|
22
|
+
/**
|
|
23
|
+
* Load the cron jobs in a directory, as values.
|
|
24
|
+
*
|
|
25
|
+
* Each file default-exports a `CronJobDefinition` — `defineCron({...})`. The
|
|
26
|
+
* filename without its extension is the record key, which is the job id:
|
|
27
|
+
* `crons/cleanup-sessions.ts` → `{ "cleanup-sessions": definition }`.
|
|
28
|
+
*
|
|
29
|
+
* Loading declares nothing: the definitions come back exactly as written, and
|
|
30
|
+
* whoever uses them decides what they are for — a backend schedules them, the
|
|
31
|
+
* build's derive step records them in the resource graph ({@link declareCrons}).
|
|
32
|
+
*
|
|
33
|
+
* Never throws for a bad file: it is skipped, logged, and passed to
|
|
34
|
+
* `options.onSkip`.
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```ts
|
|
38
|
+
* const crons = await loadCrons(path.resolve("crons"));
|
|
39
|
+
* const backend = await createRebaseBackend({ database, crons, provision: true });
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
async function loadCrons(directory, options = {}) {
|
|
43
|
+
const { crons, problems } = await readCronsDirectory(directory, options.importModule);
|
|
44
|
+
if (options.onSkip) for (const problem of problems) options.onSkip(problem);
|
|
45
|
+
return crons;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Record each cron in the project's resource graph — what `rebase resources`
|
|
49
|
+
* lists and a host reads to know a schedule exists, without running it.
|
|
50
|
+
*
|
|
51
|
+
* Keyed by the same id the scheduler, the routes and the Studio use, and built
|
|
52
|
+
* from the definitions as written: a default the scheduler fills in
|
|
53
|
+
* (`enabled: true`, `timeoutSeconds: 300`) is this process's, not something the
|
|
54
|
+
* author declared.
|
|
55
|
+
*/
|
|
56
|
+
function declareCrons(crons) {
|
|
57
|
+
for (const [id, definition] of Object.entries(crons)) declareCron(id, cronResourceOptions(definition));
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The jobs a scheduler registers, from cron values: the definitions with this
|
|
61
|
+
* process's defaults filled in.
|
|
62
|
+
*
|
|
63
|
+
* Spread first, then normalise. Rebuilding the object field by field dropped
|
|
64
|
+
* `catchUpWindowSeconds` once — and since this is the path every job takes to
|
|
65
|
+
* the scheduler, catch-up never ran for any job authored the documented way. A
|
|
66
|
+
* field added to `CronJobDefinition` tomorrow would be dropped the same way.
|
|
67
|
+
*
|
|
68
|
+
* @throws when a value has no `schedule` or no `handler`. A file like that is
|
|
69
|
+
* skipped by the loader with a reason; a value handed over in code is refused,
|
|
70
|
+
* because the caller is right there to fix it.
|
|
71
|
+
*/
|
|
72
|
+
function cronJobsFrom(crons) {
|
|
73
|
+
return Object.entries(crons).map(([id, definition]) => {
|
|
74
|
+
const def = definition;
|
|
75
|
+
if (!def || typeof def.schedule !== "string" || typeof def.handler !== "function") throw new Error(`crons["${id}"] is not a cron job: it needs a \`schedule\` string and a \`handler\` function. Author it with \`defineCron({ schedule, handler })\`.`);
|
|
76
|
+
return {
|
|
77
|
+
id,
|
|
78
|
+
definition: {
|
|
79
|
+
...def,
|
|
80
|
+
schedule: def.schedule,
|
|
81
|
+
name: def.name || id,
|
|
82
|
+
enabled: def.enabled !== false,
|
|
83
|
+
timeoutSeconds: def.timeoutSeconds ?? 300,
|
|
84
|
+
handler: def.handler
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* @deprecated Use `loadCrons` instead, which returns the jobs as a record keyed
|
|
91
|
+
* by id. Will be removed in a future release.
|
|
92
|
+
*/
|
|
93
|
+
async function loadCronJobsFromDirectory(directory, importModule = nativeDynamicImport) {
|
|
94
|
+
warnDeprecated("server.loadCronJobsFromDirectory", "`loadCronJobsFromDirectory` is deprecated; use `loadCrons(dir)`, which returns `{ [id]: definition }`.");
|
|
95
|
+
return (await legacyLoad(directory, importModule)).jobs;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* @deprecated Use `loadCrons` instead, with `onSkip` to collect the files it
|
|
99
|
+
* skipped — and `declareCrons` where the jobs belong in the resource graph,
|
|
100
|
+
* which this used to do as a side effect of loading. Will be removed in a
|
|
101
|
+
* future release.
|
|
102
|
+
*/
|
|
103
|
+
async function loadCronJobsWithDiagnostics(directory, importModule = nativeDynamicImport) {
|
|
104
|
+
warnDeprecated("server.loadCronJobsWithDiagnostics", "`loadCronJobsWithDiagnostics` is deprecated; use `loadCrons(dir, { onSkip })`, which returns `{ [id]: definition }`, and `declareCrons(crons)` to record them in the resource graph.");
|
|
105
|
+
return legacyLoad(directory, importModule);
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* What the deprecated loaders did: load, declare every job in the resource
|
|
109
|
+
* graph, and hand back normalised jobs. A job the graph refuses is skipped as a
|
|
110
|
+
* problem, as it was when the declaration ran inside the loader.
|
|
111
|
+
*/
|
|
112
|
+
async function legacyLoad(directory, importModule) {
|
|
113
|
+
const { crons, problems } = await readCronsDirectory(directory, importModule);
|
|
114
|
+
const declared = {};
|
|
115
|
+
for (const [id, definition] of Object.entries(crons)) try {
|
|
116
|
+
declareCron(id, cronResourceOptions(definition));
|
|
117
|
+
declared[id] = definition;
|
|
118
|
+
} catch (err) {
|
|
119
|
+
problems.push(describeLoadFailure(id, err));
|
|
120
|
+
}
|
|
121
|
+
return {
|
|
122
|
+
jobs: cronJobsFrom(declared),
|
|
123
|
+
problems
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Read a directory of cron files: the definitions that loaded, keyed by id, and
|
|
128
|
+
* one `<file> (<reason>)` per file that will NOT be scheduled.
|
|
129
|
+
*
|
|
130
|
+
* A job that does not load does not run, and the only trace used to be one
|
|
131
|
+
* `warn` per file in the boot log — next to nothing at 3am when the report that
|
|
132
|
+
* should have been mailed was not. So the skips are counted, summarised in one
|
|
133
|
+
* place, and answered by `GET /api/admin/cron`.
|
|
134
|
+
*
|
|
135
|
+
* Never throws, for the same reason the function loader does not: one
|
|
136
|
+
* malformed file must not take the server down, and the other jobs in the
|
|
137
|
+
* directory are still worth scheduling.
|
|
138
|
+
*/
|
|
139
|
+
async function readCronsDirectory(directory, importModule = nativeDynamicImport) {
|
|
140
|
+
const crons = {};
|
|
141
|
+
const problems = [];
|
|
142
|
+
if (!fs$1.existsSync(directory)) return {
|
|
143
|
+
crons,
|
|
144
|
+
problems
|
|
145
|
+
};
|
|
146
|
+
const files = fs$1.readdirSync(directory);
|
|
147
|
+
for (const file of files) if ((file.endsWith(".ts") || file.endsWith(".js")) && !file.startsWith(".") && !file.includes(".test.") && !file.endsWith(".d.ts") && file !== "index.ts" && file !== "index.js") {
|
|
148
|
+
const filePath = path$1.join(directory, file);
|
|
149
|
+
try {
|
|
150
|
+
const fileUrl = pathToFileURL(filePath).href;
|
|
151
|
+
const exported = (await importModule(fileUrl)).default;
|
|
152
|
+
if (!exported || typeof exported !== "object") {
|
|
153
|
+
problems.push(`${file} (no default export)`);
|
|
154
|
+
logger.warn(`[cron] ${file}: no valid default export. Skipping.`);
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
const def = exported;
|
|
158
|
+
if (typeof def.schedule !== "string" || def.schedule.trim() === "" || typeof def.handler !== "function") {
|
|
159
|
+
problems.push(`${file} (default export has no 'schedule' or no 'handler')`);
|
|
160
|
+
logger.warn(`[cron] ${file}: default export missing required 'schedule' or 'handler'. Skipping.`);
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
const id = path$1.basename(file, path$1.extname(file));
|
|
164
|
+
const definition = exported;
|
|
165
|
+
crons[id] = definition;
|
|
166
|
+
logger.info(`⏰ Loaded cron job: ${id} (${definition.schedule}${definition.timezone ? ` ${definition.timezone}` : " in the process's local time"})`);
|
|
167
|
+
} catch (err) {
|
|
168
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
169
|
+
problems.push(describeLoadFailure(file, err));
|
|
170
|
+
logger.debug(`[cron] Failed to load ${file}: ${message}`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (problems.length > 0) {
|
|
174
|
+
const syntax = problems.some((p) => p.includes("(syntax error"));
|
|
175
|
+
const other = problems.some((p) => !p.includes("(syntax error"));
|
|
176
|
+
logger.warn(`[cron] ${problems.length} cron file(s) were skipped and will NOT be scheduled:\n` + problems.map((p) => ` - ${p}`).join("\n") + "\n" + [...syntax ? [SYNTAX_ERROR_ADVICE] : [], ...other ? [" Fix these or author them with `defineCron(...)` for a typed, compile-checked contract."] : []].join("\n"));
|
|
177
|
+
}
|
|
178
|
+
return {
|
|
179
|
+
crons,
|
|
180
|
+
problems
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* The declaration a definition makes: every field a host can read without the
|
|
185
|
+
* handler. Spelled once here so the graph and the scheduler cannot disagree
|
|
186
|
+
* about what a cron is.
|
|
187
|
+
*/
|
|
188
|
+
function cronResourceOptions(definition) {
|
|
189
|
+
return {
|
|
190
|
+
schedule: definition.schedule,
|
|
191
|
+
...definition.timezone !== void 0 ? { timezone: definition.timezone } : {},
|
|
192
|
+
...definition.description !== void 0 ? { description: definition.description } : {},
|
|
193
|
+
...definition.enabled !== void 0 ? { enabled: definition.enabled } : {},
|
|
194
|
+
...definition.timeoutSeconds !== void 0 ? { timeoutSeconds: definition.timeoutSeconds } : {},
|
|
195
|
+
...definition.catchUpWindowSeconds !== void 0 ? { catchUpWindowSeconds: definition.catchUpWindowSeconds } : {},
|
|
196
|
+
...definition.name ? { label: definition.name } : {}
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
//#endregion
|
|
200
|
+
export { loadCrons as a, loadCronJobsWithDiagnostics as i, declareCrons as n, loadCronJobsFromDirectory as r, cron_loader_exports as t };
|
|
201
|
+
|
|
202
|
+
//# sourceMappingURL=cron-loader-B-u0snqq.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cron-loader-B-u0snqq.js","names":[],"sources":["../src/cron/cron-loader.ts"],"sourcesContent":["import * as fs from \"fs\";\nimport * as path from \"path\";\nimport { pathToFileURL } from \"url\";\nimport { declareCron, type CronJobDefinition, type CronResourceOptions } from \"@rebasepro/types\";\nimport { logger } from \"../utils/logger.js\";\nimport { nativeDynamicImport, type ModuleImporter } from \"../utils/dynamic-import.js\";\nimport { describeLoadFailure, SYNTAX_ERROR_ADVICE } from \"../utils/load-failure.js\";\nimport { warnDeprecated } from \"@rebasepro/common\";\nimport type { LoadDirectoryOptions } from \"../functions/function-loader.js\";\n\n/**\n * Cron jobs as values: the job id, to its definition.\n *\n * `crons: { \"nightly-report\": nightlyReport }` schedules `nightlyReport` as the\n * job `nightly-report` — the same thing a file named `crons/nightly-report.ts`\n * does. {@link loadCrons} returns exactly this shape, so\n * `crons: await loadCrons(dir)` and `cronsDir: dir` schedule the same jobs.\n *\n * A value is what the author wrote — `defineCron({...})` — with no defaults\n * filled in. The scheduler fills them in when it registers the job.\n */\nexport type CronsRecord = Record<string, CronJobDefinition>;\n\n/** A job as the scheduler registers it: its id, and its definition with defaults filled in. */\nexport interface LoadedCronJob {\n /** Job ID derived from filename (e.g. \"cleanup-sessions\"). */\n id: string;\n /** The full definition. */\n definition: CronJobDefinition;\n}\n\n/** {@link loadCronJobsWithDiagnostics}' answer: what loaded, and what did not. */\nexport interface LoadedCronJobs {\n jobs: LoadedCronJob[];\n /** One entry per file that will NOT be scheduled, with the reason. */\n problems: string[];\n}\n\n/**\n * Load the cron jobs in a directory, as values.\n *\n * Each file default-exports a `CronJobDefinition` — `defineCron({...})`. The\n * filename without its extension is the record key, which is the job id:\n * `crons/cleanup-sessions.ts` → `{ \"cleanup-sessions\": definition }`.\n *\n * Loading declares nothing: the definitions come back exactly as written, and\n * whoever uses them decides what they are for — a backend schedules them, the\n * build's derive step records them in the resource graph ({@link declareCrons}).\n *\n * Never throws for a bad file: it is skipped, logged, and passed to\n * `options.onSkip`.\n *\n * @example\n * ```ts\n * const crons = await loadCrons(path.resolve(\"crons\"));\n * const backend = await createRebaseBackend({ database, crons, provision: true });\n * ```\n */\nexport async function loadCrons(directory: string, options: LoadDirectoryOptions = {}): Promise<CronsRecord> {\n const { crons, problems } = await readCronsDirectory(directory, options.importModule);\n if (options.onSkip) for (const problem of problems) options.onSkip(problem);\n return crons;\n}\n\n/**\n * Record each cron in the project's resource graph — what `rebase resources`\n * lists and a host reads to know a schedule exists, without running it.\n *\n * Keyed by the same id the scheduler, the routes and the Studio use, and built\n * from the definitions as written: a default the scheduler fills in\n * (`enabled: true`, `timeoutSeconds: 300`) is this process's, not something the\n * author declared.\n */\nexport function declareCrons(crons: CronsRecord): void {\n for (const [id, definition] of Object.entries(crons)) {\n declareCron(id, cronResourceOptions(definition));\n }\n}\n\n/**\n * The jobs a scheduler registers, from cron values: the definitions with this\n * process's defaults filled in.\n *\n * Spread first, then normalise. Rebuilding the object field by field dropped\n * `catchUpWindowSeconds` once — and since this is the path every job takes to\n * the scheduler, catch-up never ran for any job authored the documented way. A\n * field added to `CronJobDefinition` tomorrow would be dropped the same way.\n *\n * @throws when a value has no `schedule` or no `handler`. A file like that is\n * skipped by the loader with a reason; a value handed over in code is refused,\n * because the caller is right there to fix it.\n */\nexport function cronJobsFrom(crons: CronsRecord): LoadedCronJob[] {\n return Object.entries(crons).map(([id, definition]) => {\n const def = definition as Partial<CronJobDefinition> | undefined;\n if (!def || typeof def.schedule !== \"string\" || typeof def.handler !== \"function\") {\n throw new Error(\n `crons[\"${id}\"] is not a cron job: it needs a \\`schedule\\` string and a \\`handler\\` function. ` +\n \"Author it with `defineCron({ schedule, handler })`.\"\n );\n }\n return {\n id,\n definition: {\n ...def,\n schedule: def.schedule,\n name: def.name || id,\n enabled: def.enabled !== false,\n // `??`, not `||`: zero and NaN are values the author wrote, and\n // the scheduler refuses both by name (they end up under\n // `rejected`). `|| 300` swallowed them, so a job written with\n // `0` meaning \"no limit\" ran with five minutes and was listed as\n // healthy. `Infinity` is \"none\".\n timeoutSeconds: def.timeoutSeconds ?? 300,\n handler: def.handler\n }\n };\n });\n}\n\n/**\n * @deprecated Use `loadCrons` instead, which returns the jobs as a record keyed\n * by id. Will be removed in a future release.\n */\nexport async function loadCronJobsFromDirectory(\n directory: string,\n importModule: ModuleImporter = nativeDynamicImport\n): Promise<LoadedCronJob[]> {\n warnDeprecated(\n \"server.loadCronJobsFromDirectory\",\n \"`loadCronJobsFromDirectory` is deprecated; use `loadCrons(dir)`, which returns `{ [id]: definition }`.\"\n );\n return (await legacyLoad(directory, importModule)).jobs;\n}\n\n/**\n * @deprecated Use `loadCrons` instead, with `onSkip` to collect the files it\n * skipped — and `declareCrons` where the jobs belong in the resource graph,\n * which this used to do as a side effect of loading. Will be removed in a\n * future release.\n */\nexport async function loadCronJobsWithDiagnostics(\n directory: string,\n importModule: ModuleImporter = nativeDynamicImport\n): Promise<LoadedCronJobs> {\n warnDeprecated(\n \"server.loadCronJobsWithDiagnostics\",\n \"`loadCronJobsWithDiagnostics` is deprecated; use `loadCrons(dir, { onSkip })`, which returns \" +\n \"`{ [id]: definition }`, and `declareCrons(crons)` to record them in the resource graph.\"\n );\n return legacyLoad(directory, importModule);\n}\n\n/**\n * What the deprecated loaders did: load, declare every job in the resource\n * graph, and hand back normalised jobs. A job the graph refuses is skipped as a\n * problem, as it was when the declaration ran inside the loader.\n */\nasync function legacyLoad(directory: string, importModule: ModuleImporter): Promise<LoadedCronJobs> {\n const { crons, problems } = await readCronsDirectory(directory, importModule);\n const declared: CronsRecord = {};\n for (const [id, definition] of Object.entries(crons)) {\n try {\n declareCron(id, cronResourceOptions(definition));\n declared[id] = definition;\n } catch (err) {\n problems.push(describeLoadFailure(id, err));\n }\n }\n return { jobs: cronJobsFrom(declared), problems };\n}\n\n/**\n * Read a directory of cron files: the definitions that loaded, keyed by id, and\n * one `<file> (<reason>)` per file that will NOT be scheduled.\n *\n * A job that does not load does not run, and the only trace used to be one\n * `warn` per file in the boot log — next to nothing at 3am when the report that\n * should have been mailed was not. So the skips are counted, summarised in one\n * place, and answered by `GET /api/admin/cron`.\n *\n * Never throws, for the same reason the function loader does not: one\n * malformed file must not take the server down, and the other jobs in the\n * directory are still worth scheduling.\n */\nexport async function readCronsDirectory(\n directory: string,\n importModule: ModuleImporter = nativeDynamicImport\n): Promise<{ crons: CronsRecord; problems: string[] }> {\n const crons: CronsRecord = {};\n // Aggregated so a broken job surfaces as one loud summary rather than a\n // warning buried per-file among the boot noise.\n const problems: string[] = [];\n\n if (!fs.existsSync(directory)) {\n return { crons, problems };\n }\n\n const files = fs.readdirSync(directory);\n for (const file of files) {\n if (\n (file.endsWith(\".ts\") || file.endsWith(\".js\")) &&\n // Dotfiles: notably macOS bsdtar AppleDouble sidecars (`._foo.ts`),\n // binary blobs that cannot be imported.\n !file.startsWith(\".\") &&\n !file.includes(\".test.\") &&\n !file.endsWith(\".d.ts\") &&\n file !== \"index.ts\" &&\n file !== \"index.js\"\n ) {\n const filePath = path.join(directory, file);\n try {\n const fileUrl = pathToFileURL(filePath).href;\n\n const mod = await importModule(fileUrl);\n\n const exported: unknown = mod.default;\n\n if (!exported || typeof exported !== \"object\") {\n problems.push(`${file} (no default export)`);\n logger.warn(`[cron] ${file}: no valid default export. Skipping.`);\n continue;\n }\n\n const def = exported as Record<string, unknown>;\n if (typeof def.schedule !== \"string\" || def.schedule.trim() === \"\" || typeof def.handler !== \"function\") {\n problems.push(`${file} (default export has no 'schedule' or no 'handler')`);\n logger.warn(`[cron] ${file}: default export missing required 'schedule' or 'handler'. Skipping.`);\n continue;\n }\n\n const id = path.basename(file, path.extname(file));\n const definition = exported as CronJobDefinition;\n crons[id] = definition;\n logger.info(\n `⏰ Loaded cron job: ${id} (${definition.schedule}` +\n `${definition.timezone ? ` ${definition.timezone}` : \" in the process's local time\"})`\n );\n } catch (err: unknown) {\n const message =\n err instanceof Error ? err.message : String(err);\n problems.push(describeLoadFailure(file, err));\n // At `debug`: the aggregate warning below lists every problem\n // by file, so this line was the same text a second time — and\n // when the throw is a Zod error, that text is ten lines of\n // serialised issues. The reader met it twice before reaching\n // the sentence that says what to do.\n logger.debug(`[cron] Failed to load ${file}: ${message}`);\n }\n }\n }\n\n if (problems.length > 0) {\n // Advice keyed to what went wrong, as the functions loader does:\n // `defineCron` is the remedy for a wrong export shape, not for a typo.\n const syntax = problems.some(p => p.includes(\"(syntax error\"));\n const other = problems.some(p => !p.includes(\"(syntax error\"));\n logger.warn(\n `[cron] ${problems.length} cron file(s) were skipped and will NOT be scheduled:\\n` +\n problems.map((p) => ` - ${p}`).join(\"\\n\") + \"\\n\" +\n [\n ...(syntax ? [SYNTAX_ERROR_ADVICE] : []),\n ...(other ? [\" Fix these or author them with `defineCron(...)` for a typed, compile-checked contract.\"] : [])\n ].join(\"\\n\")\n );\n }\n\n return { crons, problems };\n}\n\n/**\n * The declaration a definition makes: every field a host can read without the\n * handler. Spelled once here so the graph and the scheduler cannot disagree\n * about what a cron is.\n */\nexport function cronResourceOptions(definition: CronJobDefinition): CronResourceOptions {\n return {\n schedule: definition.schedule,\n ...(definition.timezone !== undefined ? { timezone: definition.timezone } : {}),\n ...(definition.description !== undefined ? { description: definition.description } : {}),\n ...(definition.enabled !== undefined ? { enabled: definition.enabled } : {}),\n ...(definition.timeoutSeconds !== undefined ? { timeoutSeconds: definition.timeoutSeconds } : {}),\n ...(definition.catchUpWindowSeconds !== undefined ? { catchUpWindowSeconds: definition.catchUpWindowSeconds } : {}),\n ...(definition.name ? { label: definition.name } : {})\n };\n}\n"],"mappings":"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"}
|