@starci/hfs 4.7.32 → 4.7.34
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/CHANGELOG.md +10 -0
- package/README.md +13 -6
- package/emit/contracts.mjs +4 -3
- package/emit/form-bounds-reader.ts.partial +58 -0
- package/emit/form-bounds.mjs +155 -0
- package/emit/graphql-input-bounds.mjs +97 -0
- package/emit/input-bounds.mjs +44 -0
- package/emit/operation-schemas.mjs +137 -0
- package/emit/operations-worker.mjs +2 -2
- package/emit/operations.mjs +52 -16
- package/emit/schema-worker.mjs +17 -4
- package/emit/type-schema.mjs +17 -5
- package/package.json +9 -2
- package/release/gate.mjs +18 -23
- package/release/index.mjs +1 -1
- package/release/push.mjs +4 -5
- package/runtime/engine/admission.mjs +331 -0
- package/runtime/engine/canonical-json.mjs +14 -0
- package/runtime/engine/db/blob.mjs +380 -0
- package/runtime/engine/db/event-compact.mjs +73 -0
- package/runtime/engine/db/ledger-open.mjs +38 -0
- package/runtime/engine/db/ledger-paths.mjs +87 -0
- package/runtime/engine/db/ledger.mjs +1068 -0
- package/runtime/engine/db/machine-connection.mjs +150 -0
- package/runtime/engine/db/machine-open.mjs +60 -0
- package/runtime/engine/db/machine-schema.mjs +105 -0
- package/runtime/engine/db/machine.mjs +1317 -0
- package/runtime/engine/db/provider-reservations.mjs +127 -0
- package/runtime/engine/db/provision-authority.mjs +76 -0
- package/runtime/engine/db/provision-lock.mjs +29 -0
- package/runtime/engine/db/provision-operations.mjs +146 -0
- package/runtime/engine/db/ref-value.mjs +67 -0
- package/runtime/engine/db/schema/machine.sql +920 -0
- package/runtime/engine/db/schema/runtime.sql +1113 -0
- package/runtime/engine/refuse.mjs +11 -0
- package/runtime/knowledge/hfs/canon-pins.yaml +65 -9
- package/runtime/knowledge/hfs/design-truths.yaml +149 -0
- package/runtime/knowledge/hfs/fe-stack.yaml +23 -5
- package/runtime/knowledge/hfs/rules.yaml +269 -48
- package/runtime/knowledge/hfs/slots.yaml +113 -53
- package/runtime/knowledge/patterns/be/api.yaml +46 -20
- package/runtime/knowledge/patterns/be/event-bus.yaml +19 -12
- package/runtime/knowledge/patterns/be/idempotency.yaml +265 -0
- package/runtime/knowledge/patterns/be/jobs.yaml +17 -12
- package/runtime/knowledge/patterns/be/projections.yaml +2 -2
- package/runtime/knowledge/patterns/be/queues.yaml +57 -22
- package/runtime/knowledge/patterns/be/reactors.yaml +1 -1
- package/runtime/knowledge/patterns/be/realtime.yaml +326 -27
- package/runtime/knowledge/patterns/be/saga.yaml +2 -2
- package/runtime/knowledge/sonar-gate.yaml +1 -1
- package/runtime/modules/kernel/command-policy.yaml +249 -0
- package/runtime/modules/kernel/failure-codes.yaml +211 -52
- package/runtime/modules/kernel/hook-input.yaml +6 -0
- package/runtime/modules/kernel/protected-zone.yaml +81 -0
- package/runtime/modules/kernel/removed-vocabulary.yaml +62 -0
- package/runtime/modules/kernel/roles.yaml +405 -0
- package/runtime/modules/models/registry.yaml +3 -1
- package/runtime/modules/models/runtimes.yaml +2 -0
- package/runtime/modules/schemas/application-stacks.schema.yaml +10 -2
- package/runtime/packages/cli/src/catalog.generated.mjs +46 -59
- package/runtime/packages/grammar/scripts/build-stamp.mjs +98 -0
- package/runtime/scripts/agent/codex-app-server.mjs +79 -0
- package/runtime/scripts/agent/codex-mirror-source.mjs +34 -0
- package/runtime/scripts/agent/host-agents.mjs +80 -0
- package/runtime/scripts/agent/launch-trust-policy.mjs +53 -0
- package/runtime/scripts/agent/trust-launch.mjs +191 -0
- package/runtime/scripts/agent/trust.mjs +523 -0
- package/runtime/scripts/api/fs/rename-over.mjs +21 -0
- package/runtime/scripts/api/fs/temp-path.mjs +8 -0
- package/runtime/scripts/api/fs/write-json-file.mjs +10 -0
- package/runtime/scripts/api/git/diff-names.mjs +8 -0
- package/runtime/scripts/api/git/head-time.mjs +10 -0
- package/runtime/scripts/api/git/index-lock.mjs +78 -0
- package/runtime/scripts/api/git/is-ancestor.mjs +5 -0
- package/runtime/scripts/api/git/log.mjs +5 -0
- package/runtime/scripts/api/git/merge-base-query.mjs +5 -0
- package/runtime/scripts/api/git/merge-tree.mjs +5 -0
- package/runtime/scripts/api/git/rev-list.mjs +5 -0
- package/runtime/scripts/api/git/rev-parse.mjs +13 -0
- package/runtime/scripts/api/git/status-query.mjs +5 -0
- package/runtime/scripts/api/git/worktree-list-query.mjs +5 -0
- package/runtime/scripts/api/node/spawn-node.mjs +6 -0
- package/runtime/scripts/api/npm/dist-tag.mjs +8 -0
- package/runtime/scripts/api/npm/lib.mjs +32 -0
- package/runtime/scripts/api/npm/pack-dry-run.mjs +9 -0
- package/runtime/scripts/api/npm/run-npm.mjs +6 -0
- package/runtime/scripts/api/npm/view.mjs +30 -0
- package/runtime/scripts/api/npm/whoami.mjs +8 -0
- package/runtime/scripts/api/process/hide-child-windows.mjs +31 -0
- package/runtime/scripts/api/process/kill-tree.mjs +22 -0
- package/runtime/scripts/api/process/lib.mjs +125 -0
- package/runtime/scripts/api/process/probe-guard-command.mjs +74 -0
- package/runtime/scripts/api/process/process-list.mjs +28 -0
- package/runtime/scripts/api/process/system-tool.mjs +37 -0
- package/runtime/scripts/gates/canon-digest.mjs +81 -0
- package/runtime/scripts/gates/canon-pins.mjs +16 -0
- package/runtime/scripts/gates/commit-proof.mjs +53 -0
- package/runtime/scripts/gates/gate-input.mjs +98 -0
- package/runtime/scripts/gates/gate.mjs +669 -0
- package/runtime/scripts/gates/grammar-dist.mjs +206 -0
- package/runtime/scripts/gates/runtime-package-selection.mjs +27 -0
- package/runtime/scripts/gates/test-world-run.mjs +254 -0
- package/runtime/scripts/gates/type-impact.mjs +277 -0
- package/runtime/scripts/gates/type-program.mjs +113 -0
- package/runtime/scripts/guards/assigned-command.mjs +12 -0
- package/runtime/scripts/guards/call-rights.mjs +47 -0
- package/runtime/scripts/guards/command-guard.mjs +468 -0
- package/runtime/scripts/guards/command-policy.mjs +342 -0
- package/runtime/scripts/guards/critic-reach.mjs +82 -0
- package/runtime/scripts/guards/deps-guard.mjs +150 -0
- package/runtime/scripts/guards/env-dump-verdict.mjs +89 -0
- package/runtime/scripts/guards/git-pathspec.mjs +225 -0
- package/runtime/scripts/guards/git-policy.mjs +355 -0
- package/runtime/scripts/guards/guards-root.mjs +27 -0
- package/runtime/scripts/guards/hook-install.mjs +343 -0
- package/runtime/scripts/guards/hook-io.mjs +76 -0
- package/runtime/scripts/guards/hook-verdict.mjs +12 -0
- package/runtime/scripts/guards/install-verdict.mjs +41 -0
- package/runtime/scripts/guards/kernel-seat.mjs +63 -0
- package/runtime/scripts/guards/launch-verdict.mjs +74 -0
- package/runtime/scripts/guards/op-starci.mjs +16 -0
- package/runtime/scripts/guards/op-write-scope.mjs +27 -0
- package/runtime/scripts/guards/orca-self-lifecycle.mjs +39 -0
- package/runtime/scripts/guards/process-kill-verdict.mjs +106 -0
- package/runtime/scripts/guards/protected-zone.mjs +56 -0
- package/runtime/scripts/guards/refusals.mjs +27 -0
- package/runtime/scripts/guards/release-definition.mjs +185 -0
- package/runtime/scripts/guards/release-push-gate.mjs +72 -0
- package/runtime/scripts/guards/release-record.mjs +58 -0
- package/runtime/scripts/guards/release-suite-mode.mjs +15 -0
- package/runtime/scripts/guards/rights.mjs +268 -0
- package/runtime/scripts/guards/seat-tools.mjs +47 -0
- package/runtime/scripts/guards/shell-commands.mjs +328 -0
- package/runtime/scripts/guards/supervisor-seat.mjs +80 -0
- package/runtime/scripts/guards/verdict-process.mjs +81 -0
- package/runtime/scripts/hfs/architecture/dead-exports.mjs +10 -10
- package/runtime/scripts/hfs/architecture/function-spec-providers.mjs +1 -1
- package/runtime/scripts/hfs/architecture/operation-contract-roots.mjs +53 -0
- package/runtime/scripts/hfs/architecture/reachability.mjs +1 -1
- package/runtime/scripts/hfs/architecture/sql-owner.mjs +17 -6
- package/runtime/scripts/hfs/architecture/sql-token-forms.mjs +105 -0
- package/runtime/scripts/hfs/architecture/sql-tokens.mjs +38 -52
- package/runtime/scripts/hfs/architecture/supabase-keys.mjs +261 -0
- package/runtime/scripts/hfs/architecture/transport-owner.mjs +49 -3
- package/runtime/scripts/hfs/architecture/typescript.mjs +1 -1
- package/runtime/scripts/hfs/check.mjs +6 -4
- package/runtime/scripts/hfs/coverage-scope.mjs +28 -1
- package/runtime/scripts/hfs/db-restamp-verb.mjs +56 -16
- package/runtime/scripts/hfs/declaration-shape.mjs +4 -4
- package/runtime/scripts/hfs/declaration-slots.mjs +14 -3
- package/runtime/scripts/hfs/manifest-shape.mjs +10 -6
- package/runtime/scripts/hfs/rule-params-shape.mjs +18 -6
- package/runtime/scripts/hfs/rules/a15-findings.mjs +4 -1
- package/runtime/scripts/hfs/rules/bypass-markers.mjs +22 -7
- package/runtime/scripts/hfs/rules/database-migrations.mjs +69 -14
- package/runtime/scripts/hfs/rules/database-queue-sources.mjs +9 -7
- package/runtime/scripts/hfs/rules/database-queues.mjs +68 -18
- package/runtime/scripts/hfs/rules/edition.mjs +56 -12
- package/runtime/scripts/hfs/rules/fe-contract-documents.mjs +1 -10
- package/runtime/scripts/hfs/rules/fe-stack.mjs +12 -7
- package/runtime/scripts/hfs/rules/lite-proof-lane.mjs +1 -1
- package/runtime/scripts/hfs/rules/stacks-lite-backing.mjs +75 -9
- package/runtime/scripts/hfs/rules/stacks-runtime.mjs +1 -1
- package/runtime/scripts/hfs/rules/stacks-swarm.mjs +15 -4
- package/runtime/scripts/hfs/rules/text-integrity.mjs +73 -0
- package/runtime/scripts/hfs/runtime-rules/hook-shape.mjs +74 -0
- package/runtime/scripts/hfs/runtime-rules/release-notes.mjs +44 -0
- package/runtime/scripts/hfs/sealed-members.mjs +3 -3
- package/runtime/scripts/hfs/slot-side-problems.mjs +3 -3
- package/runtime/scripts/hfs/slots.mjs +2 -1
- package/runtime/scripts/hfs/stack-deploy-config.mjs +22 -5
- package/runtime/scripts/hfs/stacks-gate.mjs +28 -7
- package/runtime/scripts/hfs/view.mjs +2 -1
- package/runtime/scripts/lib/app-data.mjs +11 -0
- package/runtime/scripts/lib/bypass-markers.mjs +272 -75
- package/runtime/scripts/lib/check-format.mjs +7 -0
- package/runtime/scripts/lib/cli-arg.mjs +120 -0
- package/runtime/scripts/lib/css-rules.mjs +220 -0
- package/runtime/scripts/lib/dom-tree.mjs +4 -0
- package/runtime/scripts/lib/escape.mjs +16 -0
- package/runtime/scripts/lib/event-items.mjs +22 -0
- package/runtime/scripts/lib/failure-code-template-errors.mjs +161 -0
- package/runtime/scripts/lib/git-dir.mjs +120 -0
- package/runtime/scripts/lib/grammar-vendor-parity.mjs +377 -0
- package/runtime/scripts/lib/guard-command.mjs +24 -0
- package/runtime/scripts/lib/hook-limits.mjs +5 -0
- package/runtime/scripts/lib/interrupt-cleanup.mjs +38 -0
- package/runtime/scripts/lib/js-lex.mjs +200 -0
- package/runtime/scripts/lib/json-formats.mjs +0 -1
- package/runtime/scripts/lib/language.mjs +94 -1
- package/runtime/scripts/lib/loosening-budgets.mjs +44 -0
- package/runtime/scripts/lib/loosening-config.mjs +361 -0
- package/runtime/scripts/lib/loosening-state.mjs +223 -0
- package/runtime/scripts/lib/menu-parts.mjs +11 -0
- package/runtime/scripts/lib/normalize.mjs +25 -0
- package/runtime/scripts/lib/op-gate.mjs +15 -0
- package/runtime/scripts/lib/redact.mjs +208 -0
- package/runtime/scripts/lib/runtime-location.mjs +21 -0
- package/runtime/scripts/lib/runtime-package-inventory.mjs +21 -0
- package/runtime/scripts/lib/runtime-package-manifests.mjs +84 -0
- package/runtime/scripts/lib/runtime-publication-tags.mjs +99 -0
- package/runtime/scripts/lib/seat-env.mjs +43 -0
- package/runtime/scripts/lib/secret-patterns.mjs +58 -9
- package/runtime/scripts/lib/shell-source.mjs +4 -0
- package/runtime/scripts/lib/source-phrases.mjs +34 -0
- package/runtime/scripts/lib/sqlite.mjs +21 -0
- package/runtime/scripts/lib/swarm-stack.mjs +2 -1
- package/runtime/scripts/lib/tar-files.mjs +39 -0
- package/runtime/scripts/lib/ts-analysis.mjs +10 -0
- package/runtime/scripts/lib/worktree-exclude.mjs +35 -0
- package/runtime/scripts/lib/worktree-record.mjs +14 -0
- package/runtime/scripts/machine/artifact-hold.mjs +52 -0
- package/runtime/scripts/machine/ask-receipts.mjs +113 -0
- package/runtime/scripts/machine/evidence-store.mjs +223 -0
- package/runtime/scripts/machine/home.mjs +216 -0
- package/runtime/scripts/machine/host-lock.mjs +184 -0
- package/runtime/scripts/machine/lock-recovery.mjs +55 -0
- package/runtime/scripts/machine/roles-contract.mjs +90 -0
- package/runtime/scripts/machine/runtime-change.mjs +20 -0
- package/runtime/scripts/machine/workflow-tree.mjs +122 -0
- package/runtime/scripts/reconciler/checklist-items.mjs +7 -0
- package/runtime/scripts/reconciler/guard-row.mjs +25 -0
- package/runtime/scripts/work/asset-slot.mjs +284 -0
- package/runtime/scripts/work/brand/brand-colour.mjs +125 -0
- package/runtime/scripts/work/brand/brand-tokens.mjs +167 -0
- package/runtime/scripts/work/brand/brand-value-source.mjs +106 -0
- package/runtime/scripts/work/brand/brand.mjs +892 -0
- package/runtime/scripts/work/draw/draw-dna-anatomy.mjs +60 -0
- package/runtime/scripts/work/draw/draw-dna-findings.mjs +256 -0
- package/runtime/scripts/work/draw/draw-dna-selector.mjs +38 -0
- package/runtime/scripts/work/draw/draw-dna.mjs +483 -0
- package/runtime/scripts/work/draw/draw-layer.mjs +305 -0
- package/runtime/scripts/work/draw/draw-rationale-page.mjs +237 -0
- package/runtime/scripts/work/draw/draw-rationale-ref.mjs +14 -0
- package/runtime/scripts/work/draw/draw-rationale.mjs +353 -0
- package/runtime/scripts/work/draw/draw-source.mjs +412 -0
- package/runtime/scripts/work/draw/draw-taste.mjs +278 -0
- package/runtime/scripts/work/draw/draw-typecheck.mjs +72 -0
- package/runtime/scripts/work/draw/rationale-findings.mjs +205 -0
- package/runtime/scripts/work/draw-grammar.mjs +175 -0
- package/runtime/scripts/work/draw-render-page.mjs +188 -0
- package/runtime/scripts/work/draw-render.mjs +626 -0
- package/runtime/scripts/work/grammar-proposal.mjs +279 -0
- package/runtime/scripts/work/png.mjs +237 -0
- package/runtime/scripts/work/render-tools.mjs +64 -0
- package/runtime/scripts/work/work-io.mjs +119 -0
- package/scaffold/add-inbox-lite.mjs +2 -2
- package/scaffold/add-plan.mjs +40 -13
- package/scaffold/add-queue-lite.mjs +372 -0
- package/scaffold/add.mjs +5 -2
- package/scaffold/app.mjs +1 -2
- package/scaffold/edition-gate.mjs +11 -7
- package/scaffold/lite-exports.mjs +10 -0
- package/scaffold/pattern-file-slots.mjs +33 -0
- package/scaffold/test-world.mjs +1 -1
- package/scaffold/yaml-block-edit.mjs +117 -0
- package/src/main.mjs +5 -4
- package/stack/deploy.mjs +7 -3
- package/stack/post-deploy.mjs +48 -0
- package/sync/index.mjs +10 -0
- package/templates/app/ci-workflows/github/workflows/stack-deploy.yml +1 -1
- package/templates/app/ci-workflows-lite/github/workflows/stack-deploy.yml +1 -1
- package/templates/app/codegen-graphql.mjs.partial +59 -12
- package/templates/app/hooks/husky/pre-commit +2 -2
- package/templates/app/hooks/husky/pre-push +10 -3
- package/templates/app/package-scripts/redis-dev.json +3 -0
- package/templates/app/package-scripts-lite/package.json +1 -0
- package/templates/app/skeleton/.starcistacks/application-stacks.yaml +36 -8
- package/templates/app/skeleton/.starcistacks/dev/README.md +1 -1
- package/templates/app/skeleton/.starcistacks/production/README.md +10 -9
- package/templates/app/skeleton/.starcistacks/production/infra/swarm/postgres-init.sh +21 -0
- package/templates/app/skeleton/.starcistacks/production/infra/swarm/recovery/restore-proof.md +1 -1
- package/templates/app/skeleton/.starcistacks/production/infra/swarm/releases/images.lock.yaml +1 -0
- package/templates/app/skeleton/.starcistacks/production/infra/swarm/stack.yaml +47 -5
- package/templates/app/skeleton/.starcistacks/production/runtime/env/KEYS.md +5 -3
- package/templates/app/skeleton/README.md +2 -0
- package/templates/app/skeleton-lite/.starcistacks/application-stacks.yaml +5 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/README.md +2 -2
- package/templates/app/skeleton-lite/.starcistacks/production/README.md +1 -1
- package/templates/app/skeleton-lite/README.md +1 -1
- package/templates/be/inbox/platform/client-operation-outcomes.sql.tpl +18 -0
- package/templates/be/inbox/platform/event-bus/errors/event-bus.error.ts.tpl +16 -0
- package/templates/be/inbox/platform/event-bus/event-bus.contracts.ts.tpl +10 -0
- package/templates/be/inbox/platform/event-bus/event-bus.port.ts.tpl +1 -1
- package/templates/be/inbox/platform/event-bus/event-bus.service.spec.ts.tpl +15 -3
- package/templates/be/inbox/platform/event-bus/event-bus.service.ts.tpl +11 -3
- package/templates/be/inbox/platform/event-bus/index.ts.tpl +2 -0
- package/templates/be/inbox/platform/event-bus/messages/event-bus.messages.ts.tpl +7 -0
- package/templates/be/inbox/platform/event-bus/persistence/event-bus.sql.ts.tpl +2 -2
- package/templates/be/inbox/platform/idempotency/errors/idempotency.error.ts.tpl +19 -0
- package/templates/be/inbox/platform/idempotency/index.ts.tpl +2 -0
- package/templates/be/inbox/platform/idempotency/messages/idempotency.messages.ts.tpl +13 -0
- package/templates/be/inbox/platform/inbox/client-operation.policy.spec.ts.tpl +32 -0
- package/templates/be/inbox/platform/inbox/client-operation.policy.ts.tpl +17 -0
- package/templates/be/inbox/platform/inbox/errors/inbox.error.ts.tpl +22 -0
- package/templates/be/inbox/platform/inbox/inbox.port.ts.tpl +21 -0
- package/templates/be/inbox/platform/inbox/inbox.service.spec.ts.tpl +334 -11
- package/templates/be/inbox/platform/inbox/inbox.service.ts.tpl +55 -4
- package/templates/be/inbox/platform/inbox/index.ts.tpl +2 -0
- package/templates/be/inbox/platform/inbox/messages/inbox.messages.ts.tpl +15 -0
- package/templates/be/inbox/platform/inbox/persistence/inbox.rows.ts.tpl +33 -0
- package/templates/be/inbox/platform/inbox/persistence/inbox.sql.ts.tpl +26 -0
- package/templates/be/inbox/platform/inbox/request-fingerprint.policy.spec.ts.tpl +76 -0
- package/templates/be/inbox/platform/inbox/request-fingerprint.policy.ts.tpl +36 -0
- package/templates/be/inbox/platform/migration.sql.tpl +1 -0
- package/templates/be/inbox/service.spec.ts.tpl +4 -4
- package/templates/be/inbox/service.ts.tpl +3 -1
- package/templates/be/inbox/webhook-door.spec.ts.tpl +3 -2
- package/templates/be/inbox/webhook-door.ts.tpl +3 -2
- package/templates/be/patterns/event-bus/platform/errors/event-bus.error.ts.tpl +3 -0
- package/templates/be/patterns/event-bus/platform/event-bus.contracts.ts.tpl +10 -0
- package/templates/be/patterns/event-bus/platform/event-bus.port.ts.tpl +2 -2
- package/templates/be/patterns/event-bus/platform/event-bus.service.spec.ts.tpl +16 -3
- package/templates/be/patterns/event-bus/platform/event-bus.service.ts.tpl +11 -3
- package/templates/be/patterns/event-bus/platform/event-envelope.policy.spec.ts.tpl +139 -50
- package/templates/be/patterns/event-bus/platform/event-envelope.policy.ts.tpl +25 -2
- package/templates/be/patterns/event-bus/platform/event-relay.service.spec.ts.tpl +139 -78
- package/templates/be/patterns/event-bus/platform/event-relay.service.ts.tpl +10 -6
- package/templates/be/patterns/event-bus/platform/event-runner.service.spec.ts.tpl +262 -159
- package/templates/be/patterns/event-bus/platform/event-runner.service.ts.tpl +32 -5
- package/templates/be/patterns/event-bus/platform/messages/event-bus.messages.ts.tpl +2 -0
- package/templates/be/patterns/event-bus/platform/persistence/entities/event-outbox.entity.ts.tpl +5 -1
- package/templates/be/patterns/event-bus/platform/persistence/event-bus.rows.ts.tpl +2 -0
- package/templates/be/patterns/event-bus/platform/persistence/event-bus.sql.ts.tpl +4 -4
- package/templates/be/patterns/event-bus/platform/persistence/migrations/1789800009000-create-event-outbox.ts.tpl +1 -0
- package/templates/be/patterns/idempotency/platform/idempotency/errors/idempotency.error.ts.tpl +19 -0
- package/templates/be/patterns/idempotency/platform/idempotency/index.ts.tpl +2 -0
- package/templates/be/patterns/idempotency/platform/idempotency/messages/idempotency.messages.ts.tpl +13 -0
- package/templates/be/patterns/idempotency/platform/inbox/client-operation.policy.spec.ts.tpl +32 -0
- package/templates/be/patterns/idempotency/platform/inbox/client-operation.policy.ts.tpl +17 -0
- package/templates/be/patterns/idempotency/platform/inbox/client-outcome-backfill.client.spec.ts.tpl +66 -0
- package/templates/be/patterns/idempotency/platform/inbox/client-outcome-backfill.client.ts.tpl +24 -0
- package/templates/be/patterns/idempotency/platform/inbox/delivery-claim-backfill.client.spec.ts.tpl +60 -0
- package/templates/be/patterns/idempotency/platform/inbox/delivery-claim-backfill.client.ts.tpl +12 -0
- package/templates/be/patterns/idempotency/platform/inbox/delivery-claim-release.client.spec.ts.tpl +49 -0
- package/templates/be/patterns/idempotency/platform/inbox/delivery-claim-release.client.ts.tpl +12 -0
- package/templates/be/patterns/idempotency/platform/inbox/effectful.decorators.ts.tpl +7 -0
- package/templates/be/patterns/idempotency/platform/inbox/errors/inbox.error.ts.tpl +22 -0
- package/templates/be/patterns/idempotency/platform/inbox/inbox.decorators.ts.tpl +17 -0
- package/templates/be/patterns/idempotency/platform/inbox/inbox.module-definition.ts.tpl +7 -0
- package/templates/be/patterns/idempotency/platform/inbox/inbox.module.ts.tpl +28 -0
- package/templates/be/patterns/idempotency/platform/inbox/inbox.options.ts.tpl +7 -0
- package/templates/be/patterns/idempotency/platform/inbox/inbox.port.ts.tpl +44 -0
- package/templates/be/patterns/idempotency/platform/inbox/inbox.service.spec.ts.tpl +388 -0
- package/templates/be/patterns/idempotency/platform/inbox/inbox.service.ts.tpl +77 -0
- package/templates/be/patterns/idempotency/platform/inbox/index.ts.tpl +10 -0
- package/templates/be/patterns/idempotency/platform/inbox/messages/inbox.messages.ts.tpl +15 -0
- package/templates/be/patterns/idempotency/platform/inbox/persistence/connection.ts.tpl +9 -0
- package/templates/be/patterns/idempotency/platform/inbox/persistence/entities/inbox-claim.entity.ts.tpl +25 -0
- package/templates/be/patterns/idempotency/platform/inbox/persistence/inbox.rows.ts.tpl +33 -0
- package/templates/be/patterns/idempotency/platform/inbox/persistence/inbox.sql.ts.tpl +39 -0
- package/templates/be/patterns/idempotency/platform/inbox/persistence/migrations/1789800005000-create-inbox-claims.ts.tpl +21 -0
- package/templates/be/patterns/idempotency/platform/inbox/persistence/migrations/1792800000000-client-operation-outcomes.ts.tpl +21 -0
- package/templates/be/patterns/idempotency/platform/inbox/request-fingerprint.policy.spec.ts.tpl +76 -0
- package/templates/be/patterns/idempotency/platform/inbox/request-fingerprint.policy.ts.tpl +36 -0
- package/templates/be/patterns/jobs/platform/fenced.processor.spec.ts.tpl +1 -0
- package/templates/be/patterns/jobs/platform/job-claim.service.spec.ts.tpl +4 -3
- package/templates/be/patterns/jobs/platform/job-claim.service.ts.tpl +2 -0
- package/templates/be/patterns/jobs/platform/job-runner.service.spec.ts.tpl +20 -2
- package/templates/be/patterns/jobs/platform/job-runner.service.ts.tpl +1 -0
- package/templates/be/patterns/jobs/platform/jobs.contracts.ts.tpl +4 -0
- package/templates/be/patterns/jobs/platform-lite/jobs.index.ts.tpl +10 -0
- package/templates/be/patterns/jobs/platform-lite/jobs.sql.tpl +44 -0
- package/templates/be/patterns/jobs/platform-lite/persistence.jobs.sql.ts.tpl +33 -0
- package/templates/be/patterns/queues/platform/bullmq-queue-transport.client.spec.ts.tpl +203 -18
- package/templates/be/patterns/queues/platform/bullmq-queue-transport.client.ts.tpl +78 -22
- package/templates/be/patterns/queues/platform/errors/queue.error.ts.tpl +19 -0
- package/templates/be/patterns/queues/platform/messages/queue.messages.ts.tpl +13 -0
- package/templates/be/patterns/queues/platform/persistence.migration.ts.tpl +1 -0
- package/templates/be/patterns/queues/platform/persistence.queue-outbox.entity.ts.tpl +4 -0
- package/templates/be/patterns/queues/platform/persistence.queue.rows.ts.tpl +2 -0
- package/templates/be/patterns/queues/platform/persistence.queue.sql.ts.tpl +4 -4
- package/templates/be/patterns/queues/platform/queue-outbox.service.spec.ts.tpl +14 -2
- package/templates/be/patterns/queues/platform/queue-outbox.service.ts.tpl +5 -2
- package/templates/be/patterns/queues/platform/queue-relay.service.spec.ts.tpl +72 -74
- package/templates/be/patterns/queues/platform/queue-relay.service.ts.tpl +1 -1
- package/templates/be/patterns/queues/platform/queue-transport.port.ts.tpl +23 -8
- package/templates/be/patterns/queues/platform/queue-worker.service.spec.ts.tpl +29 -8
- package/templates/be/patterns/queues/platform/queue-worker.service.ts.tpl +7 -3
- package/templates/be/patterns/queues/platform/queue.contracts.ts.tpl +5 -0
- package/templates/be/patterns/queues/platform/queue.index.ts.tpl +2 -0
- package/templates/be/patterns/queues/platform/queue.log-events.ts.tpl +4 -0
- package/templates/be/patterns/queues/platform/queue.module.ts.tpl +2 -1
- package/templates/be/patterns/queues/platform/queue.port.ts.tpl +1 -1
- package/templates/be/patterns/queues/platform-lite/bullmq-queue-transport.client.spec.ts.tpl +417 -0
- package/templates/be/patterns/queues/platform-lite/bullmq-queue-transport.client.ts.tpl +172 -0
- package/templates/be/patterns/queues/platform-lite/persistence.queue.sql.ts.tpl +12 -0
- package/templates/be/patterns/queues/platform-lite/queue-outbox.sql.tpl +32 -0
- package/templates/be/patterns/queues/platform-lite/queue-relay.service.spec.ts.tpl +148 -0
- package/templates/be/patterns/queues/platform-lite/queue-relay.service.ts.tpl +51 -0
- package/templates/be/patterns/queues/platform-lite/queue-transport.port.ts.tpl +163 -0
- package/templates/be/patterns/queues/platform-lite/queue.config.ts.tpl +17 -0
- package/templates/be/patterns/queues/platform-lite/queue.error.ts.tpl +25 -0
- package/templates/be/patterns/queues/platform-lite/queue.index.ts.tpl +8 -0
- package/templates/be/patterns/queues/platform-lite/queue.messages.ts.tpl +21 -0
- package/templates/be/patterns/queues/platform-lite/queue.module.ts.tpl +74 -0
- package/templates/be/patterns/queues/platform-lite/queue.options.ts.tpl +38 -0
- package/templates/be/patterns/queues/queue.queue.ts.tpl +2 -2
- package/templates/be/patterns/reactors/application.contracts.ts.tpl +2 -0
- package/templates/be/patterns/reactors/consumer.ts.tpl +5 -1
- package/templates/be/platform/graphql/graphql.contracts.ts +2 -1
- package/templates/be/platform/graphql/localize-errors.mapper.spec.ts +1 -1
- package/templates/be/skeleton/apps/__app__/src/compose/platform.ts +6 -1
- package/templates/be/skeleton/apps/__app__/src/main.ts +7 -4
- package/templates/be/skeleton/apps/cli/src/compose/platform.ts +4 -0
- package/templates/be/skeleton/apps/cli/src/main.ts +7 -4
- package/templates/be/skeleton/src/modules/platform/database/index.ts +1 -1
- package/templates/be/skeleton/src/modules/platform/http/errors/http.error.ts +22 -0
- package/templates/be/skeleton/src/modules/platform/http/fetch-http-client.client.spec.ts +235 -0
- package/templates/be/skeleton/src/modules/platform/http/fetch-http-client.client.ts +85 -0
- package/templates/be/skeleton/src/modules/platform/http/graphql-call.mapper.spec.ts +92 -0
- package/templates/be/skeleton/src/modules/platform/http/graphql-call.mapper.ts +52 -0
- package/templates/be/skeleton/src/modules/platform/http/http.decorators.ts +9 -0
- package/templates/be/skeleton/src/modules/platform/http/http.module-definition.ts +7 -0
- package/templates/be/skeleton/src/modules/platform/http/http.module.ts +19 -0
- package/templates/be/skeleton/src/modules/platform/http/http.options.ts +2 -0
- package/templates/be/skeleton/src/modules/platform/http/http.port.ts +40 -0
- package/templates/be/skeleton/src/modules/platform/http/index.ts +7 -0
- package/templates/be/skeleton/src/modules/platform/http/messages/http.messages.ts +15 -0
- package/templates/be/skeleton/src/modules/platform/ids/ids.decorators.ts +9 -0
- package/templates/be/skeleton/src/modules/platform/ids/ids.module-definition.ts +7 -0
- package/templates/be/skeleton/src/modules/platform/ids/ids.module.ts +19 -0
- package/templates/be/skeleton/src/modules/platform/ids/ids.options.ts +2 -0
- package/templates/be/skeleton/src/modules/platform/ids/ids.port.ts +5 -0
- package/templates/be/skeleton/src/modules/platform/ids/index.ts +4 -0
- package/templates/be/skeleton/src/modules/platform/ids/system-ids.service.spec.ts +15 -0
- package/templates/be/skeleton/src/modules/platform/ids/system-ids.service.ts +10 -0
- package/templates/be/skeleton/src/modules/platform/logging/correlation-request.policy.spec.ts +59 -0
- package/templates/be/skeleton/src/modules/platform/logging/correlation-request.policy.ts +21 -0
- package/templates/be/skeleton/src/modules/platform/logging/correlation.contracts.ts +16 -0
- package/templates/be/skeleton/src/modules/platform/logging/correlation.policy.spec.ts +150 -0
- package/templates/be/skeleton/src/modules/platform/logging/correlation.policy.ts +42 -0
- package/templates/be/skeleton/src/modules/platform/logging/correlation.port.ts +15 -0
- package/templates/be/skeleton/src/modules/platform/logging/correlation.service.spec.ts +23 -0
- package/templates/be/skeleton/src/modules/platform/logging/correlation.service.ts +28 -0
- package/templates/be/skeleton/src/modules/platform/logging/index.ts +14 -1
- package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.spec.ts +39 -6
- package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.ts +15 -3
- package/templates/be/skeleton/src/modules/platform/logging/logging.decorators.ts +17 -2
- package/templates/be/skeleton/src/modules/platform/logging/logging.log-events.ts +13 -1
- package/templates/be/skeleton/src/modules/platform/logging/logging.module.ts +13 -3
- package/templates/be/skeleton/src/modules/platform/logging/nest-logger.service.spec.ts +69 -0
- package/templates/be/skeleton/src/modules/platform/logging/nest-logger.service.ts +48 -0
- package/templates/be/skeleton/src/modules/platform/primitives/index.ts +1 -0
- package/templates/be/skeleton/src/modules/platform/primitives/record.policy.spec.ts +12 -0
- package/templates/be/skeleton/src/modules/platform/primitives/record.policy.ts +3 -0
- package/templates/be/skeleton-lite/apps/api/src/compose/platform.ts +6 -1
- package/templates/be/skeleton-lite/apps/cli/src/compose/platform.ts +4 -0
- package/templates/be/skeleton-lite/src/modules/platform/database/index.ts +1 -0
- package/templates/be/skeleton-lite/src/modules/platform/logging/index.ts +14 -1
- package/templates/be/skeleton-lite/src/modules/platform/logging/logging.log-events.ts +12 -0
- package/templates/be/skeleton-lite/src/modules/platform/primitives/index.ts +2 -0
- package/templates/be/test-world/world/test-apps.options.ts.tpl +2 -0
- package/templates/be/test-world/world/test-world.config.ts.tpl +1 -0
- package/upgrade/check.mjs +12 -135
- package/upgrade/file-state.mjs +1 -16
- package/upgrade/index.mjs +34 -361
- package/runtime/scripts/lib/check-code-loosening.mjs +0 -128
- package/runtime/scripts/lib/gate-loosening.mjs +0 -292
- package/templates/app/upgrade-full/.starcistacks/application-stacks.yaml +0 -89
- package/templates/app/upgrade-full/.starcistacks/dev/README.md +0 -16
- package/templates/app/upgrade-full/.starcistacks/dev/infra/compose/api.yaml +0 -13
- package/templates/app/upgrade-full/.starcistacks/dev/infra/compose/compose.yaml +0 -3
- package/templates/app/upgrade-full/.starcistacks/dev/infra/compose/postgres.yaml +0 -12
- package/templates/app/upgrade-full/.starcistacks/dev/infra/metadata.json +0 -7
- package/templates/be/skeleton-lite/apps/api/src/main.ts +0 -26
- package/templates/be/upgrade-full/apps/cli/src/app.module.ts +0 -12
- package/templates/be/upgrade-full/apps/cli/src/cli.env.ts +0 -7
- package/templates/be/upgrade-full/apps/cli/src/compose/cli.ts +0 -5
- package/templates/be/upgrade-full/apps/cli/src/compose/platform.ts +0 -9
- package/templates/be/upgrade-full/apps/cli/src/main.ts +0 -29
- package/templates/be/upgrade-full/src/features/cli/cli.module.ts +0 -7
- package/templates/be/upgrade-full/src/features/cli/migrate/subs/run.cli.ts +0 -23
|
@@ -164,4 +164,4 @@ liteAdapter:
|
|
|
164
164
|
proof: >-
|
|
165
165
|
`starci app check --wiring` proves the composing lite app's module graph with doubles (R272); declaring event-bus requires
|
|
166
166
|
its scenario-titled integration specs on `starci supabase start` (R284), including redelivery through the reactor.
|
|
167
|
-
see: [knowledge/patterns/be/
|
|
167
|
+
see: [knowledge/patterns/be/event-bus.yaml]
|
|
@@ -11,7 +11,8 @@ purpose: |-
|
|
|
11
11
|
client to a topic of that hub, scoped to the client's principal. SSE is the default door for one-way server-to-client push
|
|
12
12
|
(job progress, a status that changes, streamed tokens), in lite and in full (owner decision 2026-10-24): it is plain HTTP
|
|
13
13
|
behind the ingress, reconnects by itself and resumes from `Last-Event-ID`. The socket.io gateway stays for a channel that also
|
|
14
|
-
needs client-to-server messages, and the GraphQL subscription
|
|
14
|
+
needs client-to-server messages, and the GraphQL subscription is the restricted door for a typed schema payload on a client that already uses
|
|
15
|
+
GraphQL (BE-REALTIME-6); all three run in both editions.
|
|
15
16
|
appliesTo:
|
|
16
17
|
- backend
|
|
17
18
|
family: be
|
|
@@ -24,15 +25,27 @@ provenance:
|
|
|
24
25
|
- knowledge/patterns/be/transport.yaml
|
|
25
26
|
- knowledge/patterns/be/api-auth.yaml
|
|
26
27
|
- owner decision 2026-10-02 (feature kinds: webhooks, realtime, reactors, saga, jobs, cli; no kind imports another)
|
|
27
|
-
- owner decision 2026-10-24 (the lite edition may serve SSE, and SSE is the default door for one-way push; ruling:R212/case-5)
|
|
28
|
+
- owner decision 2026-10-24 (the lite edition may serve SSE, and SSE is the default door for one-way push; ruling:R212/case-5, amended by owner:lite-graphql-subscription-door and ruling:R212/case-7 for the GraphQL subscription door)
|
|
28
29
|
limitations: |-
|
|
29
30
|
Static checks prove the door's role, that the hub is the only thing injected and that a subscribed topic is built from the
|
|
30
31
|
principal. They do not prove the topic naming is collision free across channels, that an initial snapshot is consistent with
|
|
31
32
|
the stream, or the behaviour of the socket under reconnects; the three `realtime/*` scenarios prove delivery, isolation and
|
|
32
33
|
cleanup on the real stack. Nothing proves the ingress settings of an SSE channel (no compression, no buffering, HTTP/2, an
|
|
33
|
-
idle timeout above the heartbeat): they are reviewed against the stack files and a real stream. The hub is
|
|
34
|
-
its subscriptions and its replay buffer live in one process, so
|
|
35
|
-
|
|
34
|
+
idle timeout above the heartbeat): they are reviewed against the stack files and a real stream. The memory hub is
|
|
35
|
+
single-process: its subscriptions and its replay buffer live in one process, so it serves tests and one replica only
|
|
36
|
+
(STACKS_REALTIME_SCALED refuses it with more); a shared hub (BE-REALTIME-7) carries pushes and replay across replicas, and
|
|
37
|
+
its socket.io adapters have no connection state recovery, so a socket client resyncs by query. A replay returns at most 64 frames
|
|
38
|
+
per topic; Postgres sweeps frames older than 15 minutes, Redis expires a topic stream after 15 minutes without a retained publish,
|
|
39
|
+
and memory keeps its bounded buffer without a TTL. A client whose
|
|
40
|
+
`Last-Event-ID` is older or unknown receives the newest retained frames with no signal
|
|
41
|
+
of the gap, so a screen refetches its snapshot after any reconnect. A frame with no string `id` is live-only in the memory
|
|
42
|
+
and Redis hubs; Postgres retains it and reloads it after a LISTEN loss by its sequence position, but no client can name it as
|
|
43
|
+
`Last-Event-ID`. Only the Postgres hub publishes inside the publisher's transaction, the other hubs push at once. Each open
|
|
44
|
+
subscription on a shared hub keeps its own last position, initially where its topic stood at join. After a lost LISTEN or subscriber connection
|
|
45
|
+
the hub reloads each topic after the minimum last position of its open subscriptions; each subscription drops positions it has
|
|
46
|
+
already taken, so a new subscription cannot skip frames owed to an older one and replay, reload and held notifications do not
|
|
47
|
+
duplicate a retained frame. A failed reload keeps live delivery paused and retries until the current loss generation reloads.
|
|
48
|
+
graphql-ws has no replay: a screen on a GraphQL subscription refetches its snapshot on reconnect.
|
|
36
49
|
rules:
|
|
37
50
|
- id: BE-REALTIME-1
|
|
38
51
|
title: A channel is a kind folder with one door, its module and its own spec
|
|
@@ -47,8 +60,8 @@ rules:
|
|
|
47
60
|
`<channel>.gateway.spec.ts`, or `transport/graphql/<channel>.subscription.ts` (one `@Resolver` class whose operations are `@Subscription`) with
|
|
48
61
|
`<channel>.subscription.spec.ts`, plus optional `dto/` (`<name>.{input,type,args}.ts`). A door carries no query, mutation, field
|
|
49
62
|
resolver or plain REST route (`@Get`, `@Post` ...; the `@Sse` stream is the event-stream evidence of BE_REST_DOOR_EVIDENCE only in the
|
|
50
|
-
sse slot). The kind imports only `modules/*`, never another kind, and only an api-kind app composes its module.
|
|
51
|
-
|
|
63
|
+
sse slot). The kind imports only `modules/*`, never another kind, and only an api-kind app composes its module. All three doors
|
|
64
|
+
run in lite and in full; BE-REALTIME-6 chooses between them.
|
|
52
65
|
rationale: |-
|
|
53
66
|
One file per door keeps a protocol's machinery in one place, and keeping queries and mutations out guarantees that what a
|
|
54
67
|
client can do through a realtime door is listen.
|
|
@@ -106,13 +119,13 @@ rules:
|
|
|
106
119
|
requirement: |-
|
|
107
120
|
A realtime door is thin (R203 `BE_FEATURE_THIN`): it maps the subscription or the message and makes one delegating call
|
|
108
121
|
to the hub. The only value it injects is the `RealtimeHub` of `platform/realtime` (`subscribe(topic, options?)` returns an
|
|
109
|
-
async iterable of frames, `publish(topic, frame)` pushes one; a topic is a typed `RealtimeTopic<T>` built by the owning domain
|
|
122
|
+
async iterable of frames, `publish(topic, frame)` pushes one and settles once the hub accepted it; a topic is a typed `RealtimeTopic<T>` built by the owning domain
|
|
110
123
|
contract, never a string written in a door). An SSE controller calls `hub.stream(topic, { after })` instead, which returns the
|
|
111
124
|
`Observable<MessageEvent>` Nest needs (the hub maps its own subscription with `eventStream` of `platform/realtime`): the
|
|
112
125
|
opening frame, one event per frame with its `id`, and the comment heartbeat. No `EntityManager`, `CommandBus`, `QueryBus`, event bus, queue or
|
|
113
126
|
service of `modules/domain` is injected: a domain service may write, so it is not provably read-only. The data a client
|
|
114
127
|
needs is pushed by a reactor (or by the domain service that commits the change, in an app with no event bus); the socket
|
|
115
|
-
library and the pub/sub engine (`socket.io`, `graphql-subscriptions`, `graphql-ws`) are imported only by `platform/realtime`,
|
|
128
|
+
library and the pub/sub engine (`socket.io`, `@socket.io/postgres-adapter`, `@socket.io/redis-adapter`, `graphql-subscriptions`, `graphql-ws`, `pg` for LISTEN and `ioredis`) are imported only by `platform/realtime`,
|
|
116
129
|
and so is the heartbeat timer (`setInterval` is owned by `platform/realtime`, `infraOwners`). Because the door reads only the
|
|
117
130
|
hub, a snapshot cannot be sent on connect from the door: the client reads the current state through a query operation first
|
|
118
131
|
(or again on every open), and the stream carries the changes.
|
|
@@ -138,7 +151,7 @@ rules:
|
|
|
138
151
|
listen to another's channel. The reactor that pushes uses the same topic builder from the owning domain contract, never a
|
|
139
152
|
hand-written string. The default-deny guards apply to the door like to any other; an SSE stream is never `@Public`. The
|
|
140
153
|
browser `EventSource` cannot send an `Authorization` header, so the front end reads the stream with `fetch` and the bearer
|
|
141
|
-
access token (modules/api/sse read by a
|
|
154
|
+
access token (an operation of modules/api/sse read by a useSubscribe<Channel>Swr hook); a token in the URL is refused because it ends up in logs. The `Last-Event-ID` header is client
|
|
142
155
|
input like any other: it is passed as `{ after }` to `hub.subscribe` and only picks where the replay resumes inside the
|
|
143
156
|
principal's own topic, it never names a topic.
|
|
144
157
|
rationale: |-
|
|
@@ -160,6 +173,7 @@ rules:
|
|
|
160
173
|
hfsRules: [R155]
|
|
161
174
|
requirement: |-
|
|
162
175
|
Declaring the pattern `realtime` requires e2e or integration tests titled `realtime/<scenario>: ...` for each scenario
|
|
176
|
+
`push-crosses-replicas` and `resume-after-reconnect` (two api replicas, BE-REALTIME-7),
|
|
163
177
|
`subscriber-receives-push` (a connected client receives the frame a reactor pushes after the domain event),
|
|
164
178
|
`foreign-topic-is-isolated` (another principal subscribed to the same channel receives nothing) and
|
|
165
179
|
`disconnect-releases-subscription` (after the client disconnects the hub holds no subscription for it). The client is a real
|
|
@@ -191,8 +205,8 @@ rules:
|
|
|
191
205
|
alone (`infraOwners` `setInterval`). The hub retains the last frames of a topic that carry a string `id` in a bounded
|
|
192
206
|
buffer (a fixed number of frames per topic and of topics, the oldest dropped first), so a client that reconnects with
|
|
193
207
|
`Last-Event-ID` receives what it missed; an id the buffer no longer holds replays the whole buffer, and the client refetches
|
|
194
|
-
its snapshot through the query operation on every open. The hub
|
|
195
|
-
|
|
208
|
+
its snapshot through the query operation on every open. The memory hub keeps that buffer in one process; a shared hub keeps it in
|
|
209
|
+
the `realtime_events` table or a Redis Stream, so a reconnect that lands on any replica resumes (BE-REALTIME-7). At the ingress, the
|
|
196
210
|
stream route is never compressed or buffered (Nest sends `X-Accel-Buffering: no` and `Cache-Control: no-cache, no-transform`;
|
|
197
211
|
Traefik's compress middleware excludes `text/event-stream`, a Caddy route sets no `encode` and `flush_interval -1`), the edge
|
|
198
212
|
speaks HTTP/2 to the browser so the six-connection limit of HTTP/1.1 per origin does not starve tabs, and an Express
|
|
@@ -212,9 +226,230 @@ rules:
|
|
|
212
226
|
- BE_INFRA_OWNER
|
|
213
227
|
manual:
|
|
214
228
|
- Confirm the ingress route of the api sets no compression and no response buffering for text/event-stream, and that its idle timeout is above the heartbeat.
|
|
215
|
-
- Confirm
|
|
229
|
+
- Confirm an api that runs more than one replica selects a shared hub (STACKS_REALTIME_SCALED refuses the memory hub there).
|
|
216
230
|
- Confirm the replay bounds fit the channel (frames per topic, number of topics).
|
|
217
231
|
relatedExamples: [realtime]
|
|
232
|
+
- id: BE-REALTIME-6
|
|
233
|
+
title: The door follows what is pushed and which way, not the API default
|
|
234
|
+
kind: mandatory
|
|
235
|
+
hfsRules: [R184, R212, R90, R185, R186]
|
|
236
|
+
requirement: |-
|
|
237
|
+
The API's default door is GraphQL (BE-TRANSPORT), but realtime is a separate decision: what is pushed and in which direction
|
|
238
|
+
picks the door, in both editions.
|
|
239
|
+
|
|
240
|
+
Choosing the door (both editions):
|
|
241
|
+
- One-way server-to-client push (a status that changes, job progress, streamed AI tokens): SSE, the default. It is plain HTTP that
|
|
242
|
+
passes every proxy, ingress and HTTP/2, the browser reconnects by itself and resumes from `Last-Event-ID` (the hub replays),
|
|
243
|
+
and it is the cheapest per connection.
|
|
244
|
+
- A push of a schema type the screen already reads through GraphQL, when the typed payload matters (the whole `Order` with its
|
|
245
|
+
generated type): the GraphQL subscription, restricted. It shares the schema, the codegen and the guards of the queries. Its
|
|
246
|
+
limits: graphql-ws has no replay, so on every reconnect the screen refetches its snapshot with a query; it costs more per
|
|
247
|
+
connection; it needs the WebSocket upgrade at the ingress. Use it only when the client already uses GraphQL AND needs the typed
|
|
248
|
+
payload; otherwise SSE.
|
|
249
|
+
- Two-way messages (chat, presence, collaborative input): the WebSocket door (socket.io gateway), the only door where the client
|
|
250
|
+
sends messages. It needs the WebSocket upgrade, plus sticky sessions when the long-polling fallback is on.
|
|
251
|
+
|
|
252
|
+
Every door, in both editions: it reads and pushes only (no write, query, mutation or REST route); its topic is built from
|
|
253
|
+
`@CurrentPrincipal()` and it is never `@Public`; a token never travels in the URL; only `platform/realtime` imports the engine
|
|
254
|
+
(`graphql-ws`, `graphql-subscriptions`, `socket.io`); three `realtime/*` scenarios prove delivery, isolation and cleanup. On the
|
|
255
|
+
front end the reader of a stream, a subscription or a socket is owned by the transport client layer, exposed as an operation and
|
|
256
|
+
consumed only through an SWR subscription hook that revalidates its snapshot query when the stream opens, with no fetch in an effect
|
|
257
|
+
(knowledge/patterns/fe/realtime.yaml).
|
|
258
|
+
|
|
259
|
+
Hub (both editions): the hub default is Redis when Redis is declared (lite or full), Postgres LISTEN/NOTIFY when lite declares no
|
|
260
|
+
Redis, and memory for tests or a single replica only; the memory hub is non-scale. Scale is the hub adapter behind the one
|
|
261
|
+
`RealtimeHub` port, never a different door. The Redis and Postgres hubs, the refusal of a stack with more than one api replica on
|
|
262
|
+
the memory hub (STACKS_REALTIME_SCALED) and the two-replica scenarios are the realtime scale rule BE-REALTIME-7.
|
|
263
|
+
|
|
264
|
+
Lite (ruling:R212/case-10, owner:lite-vector-is-pgvector):
|
|
265
|
+
ONE instance type, one api deployable, scales horizontally and stateless, with no sticky sessions, on Postgres/Supabase plus
|
|
266
|
+
Redis when a declared capability needs it. Vector search uses Supabase pgvector. The three doors above are served from that api (the GraphQL subscription door is new
|
|
267
|
+
in lite, ruling:R212/case-7). A lite api with more than one replica uses the Redis hub when Redis is
|
|
268
|
+
declared, else the Postgres hub. The example is the SSE `booking-status` channel.
|
|
269
|
+
|
|
270
|
+
Full: the complete infrastructure, several services with Redis (ioredis, BullMQ), and the api may run several replicas on the Redis hub (BE-REALTIME-7). The three doors and the choice above are the same as lite; no door is full-only. The example is the
|
|
271
|
+
GraphQL subscription `order-status` channel, where the typed `Order` payload matters; a screen that reads it refetches the order on
|
|
272
|
+
reconnect.
|
|
273
|
+
rationale: |-
|
|
274
|
+
A reader who knows the API is GraphQL assumes a GraphQL subscription everywhere; the push is cheaper, easier to proxy and
|
|
275
|
+
resumable over SSE, so the heavier door is chosen only for what only it gives.
|
|
276
|
+
cases:
|
|
277
|
+
- id: case-1
|
|
278
|
+
when: A booking that gets confirmed after a job, shown on a screen that already reads it through a query
|
|
279
|
+
write: 'SSE: transport/sse/booking-status.controller.ts; the front end reads the stream through a useSubscribe<Channel>Swr hook and the query operation carries the snapshot.'
|
|
280
|
+
- id: case-2
|
|
281
|
+
when: An order whose whole typed payload the screen renders, on a client that already uses GraphQL
|
|
282
|
+
write: 'GraphQL subscription: transport/graphql/order-status.subscription.ts; on every reconnect the screen refetches the order with its query because graphql-ws has no replay.'
|
|
283
|
+
verification:
|
|
284
|
+
automated:
|
|
285
|
+
- BE_REALTIME_SHAPE
|
|
286
|
+
- HFS_EDITION_INVALID
|
|
287
|
+
- HFS_EDITION_FORBIDDEN_PRESENT
|
|
288
|
+
- HFS_STARCIWORK_LITE_FORBIDDEN
|
|
289
|
+
- HFS_BACKING_UNDECLARED
|
|
290
|
+
- HFS_BACKING_UNUSED
|
|
291
|
+
- BE_INFRA_OWNER
|
|
292
|
+
- BE_REALTIME_WRITES
|
|
293
|
+
- BE_REALTIME_TOPIC_SCOPE
|
|
294
|
+
manual:
|
|
295
|
+
- Confirm a GraphQL subscription channel serves a client that already uses GraphQL and needs the typed payload, and that its screen refetches the snapshot on reconnect.
|
|
296
|
+
- "Confirm that a stack with more than one api replica uses a shared hub (BE-REALTIME-7)."
|
|
297
|
+
relatedExamples: [realtime]
|
|
298
|
+
- id: BE-REALTIME-7
|
|
299
|
+
title: A realtime channel scales horizontally on one hub port with a shared hub
|
|
300
|
+
kind: mandatory
|
|
301
|
+
hfsRules: [R90, R266, R155]
|
|
302
|
+
requirement: |-
|
|
303
|
+
Both editions run N api replicas. `platform/realtime` keeps ONE port, `RealtimeHub` (`publish(topic, frame, within?): Promise<void>`
|
|
304
|
+
called by a reactor, or by the domain service that owns the state change and passes its own transaction as `within`,
|
|
305
|
+
`subscribe(topic, { after })` for GraphQL and socket doors, `stream(topic, { after })` for the SSE door), so a door never
|
|
306
|
+
changes between a single replica and N replicas: only the hub binding does. Three adapters implement the port:
|
|
307
|
+
`MemoryRealtimeHub` (non-scale: one process, tests and a single-replica dev run, replay from an in-process ring buffer),
|
|
308
|
+
`PostgresRealtimeHub` (stateless: `publish` takes a per-topic advisory lock, writes a `realtime_events` row and NOTIFYs, in the
|
|
309
|
+
publisher's transaction when it passes one, so the push goes out at commit and a rolled-back change pushes nothing; every
|
|
310
|
+
replica LISTENs and fans out to its local subscribers; replay reads the table after the `Last-Event-ID`; when the LISTEN
|
|
311
|
+
connection is lost the hub holds notifications and reloads each followed topic after the minimum last position of its open
|
|
312
|
+
subscriptions. Each subscription starts at the topic's join position and advances as it takes entries, dropping any position
|
|
313
|
+
already taken, so an older subscription receives its outage frames once even when a new subscription joins the same topic.
|
|
314
|
+
A failed live row load takes the same loss path; a failed reload keeps the pause and retries through the injected Timer after
|
|
315
|
+
RECONNECT_DELAY_MS. Only a successful reload of the current loss generation releases and drains held notifications; a second
|
|
316
|
+
loss during a reload leaves the pause to the newer reload. The table is bounded by a fenced TTL sweep job that runs batch after batch) and
|
|
317
|
+
`RedisRealtimeHub` (`publish` runs one atomic script: XADD to a Redis Stream per topic with `MAXLEN ~`, PEXPIRE and PUBLISH;
|
|
318
|
+
replay reads the stream with XRANGE and resumes after the newest entry carrying the id; when the subscriber connection comes
|
|
319
|
+
back the hub reloads each followed topic after the minimum last stream id of its open subscriptions, with the same per-subscription
|
|
320
|
+
duplicate suppression and paused retry policy as Postgres; ioredis, every key and channel carries the
|
|
321
|
+
app prefix `REALTIME_REDIS_PREFIX`). The app selects the hub with `REALTIME_HUB=memory|postgres|redis`, parsed once by
|
|
322
|
+
`parseRealtimeConfig` and validated at boot; unset, the parser defaults to `redis` when `REALTIME_REDIS_URL` is declared (lite
|
|
323
|
+
and full), `postgres` when lite declares no Redis, and never `memory`, so a scaled deploy cannot fall back to the in-process
|
|
324
|
+
hub unseen. All three
|
|
325
|
+
adapters are legal in both editions; select `redis` when Redis is declared (lite with Redis, and full), `postgres` when it is
|
|
326
|
+
not, and `memory` only for tests or one replica. A Postgres hub LISTENs on `REALTIME_DB_URL` when declared, else on the
|
|
327
|
+
primary URL, and proves the connection at boot by hearing a probe the hub sends from another session, so a transaction pooler
|
|
328
|
+
fails the boot instead of delivering nothing. The Redis hub reads its address from the secret `REALTIME_REDIS_URL`
|
|
329
|
+
(password, ACL user and `rediss://` included). The stack check `STACKS_REALTIME_SCALED` refuses an api that composes
|
|
330
|
+
a realtime feature, runs more than one replica and does not select `postgres` or `redis`. The socket.io adapters live in
|
|
331
|
+
`platform/realtime` too and `main.ts` installs the one the hub selects through `realtimeIoAdapter(app)`: the default
|
|
332
|
+
`IoAdapter` on the memory hub, `PostgresIoAdapter` (`@socket.io/postgres-adapter`, payloads of 8 KB or more through the
|
|
333
|
+
`socket_io_attachments` table) or `RedisIoAdapter` (`createShardedAdapter`, Redis 7 or later). Both force
|
|
334
|
+
`transports: ['websocket']`, so on either shared adapter a socket is one connection to one replica, long-polling is off and
|
|
335
|
+
no sticky session exists. The memory hub's default IoAdapter also accepts polling; the canonical client uses websocket only in every mode.
|
|
336
|
+
Neither adapter has connection state recovery, and graphql-ws has no replay, so a socket or GraphQL client refetches its
|
|
337
|
+
snapshot with a query on every reconnect. The engines (`socket.io`, `@socket.io/postgres-adapter`, `@socket.io/redis-adapter`,
|
|
338
|
+
`graphql-ws`, `graphql-subscriptions`, `pg` for LISTEN and `ioredis`) are imported only by `platform/realtime`
|
|
339
|
+
(`BE_INFRA_OWNER`). The tables are migrations of the app (`realtime_events` and, for the Postgres socket adapter,
|
|
340
|
+
`socket_io_attachments`: a Supabase migration in lite, a TypeORM migration of `platform/realtime` in full). Declaring realtime
|
|
341
|
+
adds two scenarios, proven with two api replicas: `realtime/push-crosses-replicas` (published through replica A, received on
|
|
342
|
+
replica B) and `realtime/resume-after-reconnect` (`Last-Event-ID` sent to the other replica receives the missed frames).
|
|
343
|
+
rationale: |-
|
|
344
|
+
A hub in the process memory loses every push that was published on another replica and every replay buffer a reconnect lands
|
|
345
|
+
away from. Shared state in Postgres or Redis, a websocket-only transport and a client that resyncs by query make any replica
|
|
346
|
+
able to serve any request or reconnect, with no sticky session and no state a replica needs from another.
|
|
347
|
+
cases:
|
|
348
|
+
- id: case-1
|
|
349
|
+
when: SSE, non-scale (tests, one replica)
|
|
350
|
+
write: |-
|
|
351
|
+
The door is the same in every mode. Only the hub binding differs.
|
|
352
|
+
// features/realtime/booking-status/transport/sse/booking-status.controller.ts
|
|
353
|
+
@Controller('booking-status')
|
|
354
|
+
export class BookingStatusController {
|
|
355
|
+
constructor(@InjectRealtimeHub() private readonly hub: RealtimeHub) {}
|
|
356
|
+
@Sse('stream')
|
|
357
|
+
stream(@CurrentPrincipal() principal: Principal, @Headers('last-event-id') lastEventId?: string): Observable<MessageEvent> {
|
|
358
|
+
return this.hub.stream(bookingStatusTopic(principal.id), { after: lastEventId })
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
// app root: REALTIME_HUB=memory -> RealtimeModule.register({ isGlobal: true, hub: 'memory' }); replay comes from the
|
|
362
|
+
// in-process buffer and a reconnect must land on the same process.
|
|
363
|
+
- id: case-2
|
|
364
|
+
when: SSE, scale on Postgres (lite without Redis; full may too)
|
|
365
|
+
write: |-
|
|
366
|
+
The same controller. The app root binds the Postgres hub and the stack runs N replicas with no sticky routing:
|
|
367
|
+
// be/apps/api/src/compose/platform.ts
|
|
368
|
+
RealtimeModule.register({ isGlobal: true, ...withConnection(options.realtime, PRIMARY_ENTITY_MANAGER, options.database.url) })
|
|
369
|
+
// stack.yaml: environment REALTIME_HUB: postgres, deploy.replicas: 2
|
|
370
|
+
publish writes the realtime_events row and NOTIFYs; every replica LISTENs and writes the frame to its local streams; a
|
|
371
|
+
reconnect to ANY replica sends Last-Event-ID and the hub reads the missed rows from the table. A domain service that owns the
|
|
372
|
+
change publishes inside its own transaction (`hub.publish(topic, frame, tx)`), so the confirmation and its push commit together. At the ingress the stream is
|
|
373
|
+
not buffered or compressed and the idle timeout is above the heartbeat. Migration: supabase/migrations/<ts>_realtime-events.sql
|
|
374
|
+
(lite) or platform/realtime/persistence/migrations/<epochMs>-create-realtime-events.ts (full).
|
|
375
|
+
- id: case-3
|
|
376
|
+
when: SSE, scale on Redis (Redis declared, lite or full)
|
|
377
|
+
write: |-
|
|
378
|
+
The same controller. REALTIME_HUB=redis binds RedisRealtimeHub: publish runs one atomic script that XADDs `<prefix>:<topic>`
|
|
379
|
+
with MAXLEN ~ 64, expires the key and PUBLISHes the frame on the `<prefix>` channel; replay is XRANGE of the stream, resuming after the newest entry whose id equals the Last-Event-ID; no table and no sweep work (its
|
|
380
|
+
retention tick is a no-op).
|
|
381
|
+
RealtimeModule.register({ isGlobal: true, ...withConnection(options.realtime, ORDER_ENTITY_MANAGER, options.database.url) })
|
|
382
|
+
// REALTIME_HUB=redis, the secret REALTIME_REDIS_URL, REALTIME_REDIS_DB (and REALTIME_REDIS_PREFIX when the server is shared);
|
|
383
|
+
// Redis 7 or later for the sharded socket.io adapter.
|
|
384
|
+
- id: case-4
|
|
385
|
+
when: GraphQL subscription, non-scale
|
|
386
|
+
write: |-
|
|
387
|
+
// features/realtime/order-status/transport/graphql/order-status.subscription.ts (same in every mode)
|
|
388
|
+
@Subscription(() => OrderStatusChangedType, { name: 'orderStatusChanged', resolve: (frame: OrderStatusFrame) => frame })
|
|
389
|
+
@Roles('member')
|
|
390
|
+
orderStatusChanged(@CurrentPrincipal() principal: Principal, @Args('input') input: OrderStatusInput) {
|
|
391
|
+
return this.hub.subscribe(orderStatusTopic(principal.id, input.orderId))
|
|
392
|
+
}
|
|
393
|
+
// REALTIME_HUB=memory replaces the graphql-subscriptions PubSub, which is not for production.
|
|
394
|
+
- id: case-5
|
|
395
|
+
when: GraphQL subscription, scale (Redis or Postgres hub)
|
|
396
|
+
write: |-
|
|
397
|
+
The same resolver. graphql-ws rides ONE WebSocket, so no stickiness is needed; the hub fans the frame out through Redis
|
|
398
|
+
(the full-app order-status example, REALTIME_HUB=redis) or Postgres LISTEN/NOTIFY. graphql-ws has no replay: the screen
|
|
399
|
+
refetches its snapshot query on every reconnect, in every mode.
|
|
400
|
+
- id: case-6
|
|
401
|
+
when: WebSocket (socket.io), non-scale
|
|
402
|
+
write: |-
|
|
403
|
+
The gateway is identical in every mode. `main.ts` calls `app.useWebSocketAdapter(realtimeIoAdapter(app))`; on the memory hub
|
|
404
|
+
that is the default Nest IoAdapter, in process.
|
|
405
|
+
- id: case-7
|
|
406
|
+
when: WebSocket, scale on Postgres
|
|
407
|
+
write: |-
|
|
408
|
+
// platform/realtime/postgres-io.adapter.ts (installed by realtimeIoAdapter on REALTIME_HUB=postgres)
|
|
409
|
+
export class PostgresIoAdapter extends IoAdapter {
|
|
410
|
+
constructor(app: INestApplicationContext, private readonly pool: Pool, private readonly build: typeof createAdapter, logger: Logger) {
|
|
411
|
+
super(app)
|
|
412
|
+
pool.on('error', (cause) => logger.warn(RealtimeLogEvent.PoolFailed, { cause: cause.message })) // an idle client lost in a failover must not crash the replica
|
|
413
|
+
}
|
|
414
|
+
override createIOServer(port: number, options?: Partial<ServerOptions>): Server {
|
|
415
|
+
const server: Server = super.createIOServer(port, { ...options, transports: ['websocket'] }) // no polling, so no sticky session
|
|
416
|
+
server.adapter(this.build(this.pool, { tableName: SOCKET_IO_ATTACHMENTS_TABLE, errorHandler: this.report }))
|
|
417
|
+
return server
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
The pool is built by RealtimeModule from the session-mode database URL (LISTEN does not work through a transaction pooler).
|
|
421
|
+
- id: case-8
|
|
422
|
+
when: WebSocket, scale on Redis
|
|
423
|
+
write: |-
|
|
424
|
+
// platform/realtime/redis-io.adapter.ts (installed on REALTIME_HUB=redis)
|
|
425
|
+
export class RedisIoAdapter extends IoAdapter {
|
|
426
|
+
constructor(app: INestApplicationContext, private readonly publisher: Redis, private readonly subscriber: Redis, private readonly build: typeof createShardedAdapter, logger: Logger, private readonly channelPrefix: string) { super(app) }
|
|
427
|
+
override createIOServer(port: number, options?: Partial<ServerOptions>): Server {
|
|
428
|
+
const server: Server = super.createIOServer(port, { ...options, transports: ['websocket'] })
|
|
429
|
+
server.adapter(this.build(this.publisher, this.subscriber, { channelPrefix: this.channelPrefix })) // two connections, errors logged, channels prefixed
|
|
430
|
+
return server
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
verification:
|
|
434
|
+
automated:
|
|
435
|
+
- BE_INFRA_OWNER
|
|
436
|
+
- STACKS_SECRET_NOT_EXTERNAL
|
|
437
|
+
- STACKS_SECRET_UNVERSIONED
|
|
438
|
+
- STACKS_SECRET_ENV_LITERAL
|
|
439
|
+
- STACKS_PORT_PUBLISHED
|
|
440
|
+
- STACKS_SECOND_INGRESS
|
|
441
|
+
- STACKS_DNS_UNDECLARED
|
|
442
|
+
- STACKS_HOST_UNROUTED
|
|
443
|
+
- STACKS_TLS_UNDECLARED
|
|
444
|
+
- STACKS_PLACEMENT_MISSING
|
|
445
|
+
- STACKS_HEALTHCHECK_MISSING
|
|
446
|
+
- STACKS_UPDATE_POLICY_MISSING
|
|
447
|
+
- STACKS_REALTIME_SCALED
|
|
448
|
+
- BE_PATTERN_SPEC_MISSING
|
|
449
|
+
manual:
|
|
450
|
+
- Confirm the ingress holds no sticky-session declaration for the api, and that polling is off in the socket.io client configuration.
|
|
451
|
+
- Confirm a Postgres hub connects through a session-mode URL and that the realtime tables are migrations of the app.
|
|
452
|
+
relatedExamples: [realtime]
|
|
218
453
|
files:
|
|
219
454
|
- path: src/features/realtime/<channel>/index.ts
|
|
220
455
|
slot: be.feature.realtime
|
|
@@ -305,16 +540,78 @@ files:
|
|
|
305
540
|
template: realtime/channel.controller.spec.ts.tpl
|
|
306
541
|
role: Unit spec of the SSE controller beside it.
|
|
307
542
|
optional: true
|
|
308
|
-
- path: src/modules/platform/realtime/realtime.
|
|
543
|
+
- path: src/modules/platform/realtime/realtime.port.ts
|
|
544
|
+
slot: be.platform
|
|
545
|
+
role: RealtimeHub, the one port of the realtime kind (publish, subscribe, stream) and the typed topic; its three adapters are the files below.
|
|
546
|
+
optional: true
|
|
547
|
+
- path: src/modules/platform/realtime/memory-realtime-hub.service.ts
|
|
548
|
+
slot: be.platform
|
|
549
|
+
role: "MemoryRealtimeHub, the non-scale adapter: one process, tests and a single-replica dev run, replay from an in-process ring buffer."
|
|
550
|
+
spec: src/modules/platform/realtime/memory-realtime-hub.service.spec.ts
|
|
551
|
+
optional: true
|
|
552
|
+
- path: src/modules/platform/realtime/postgres-realtime-hub.service.ts
|
|
553
|
+
slot: be.platform
|
|
554
|
+
role: "PostgresRealtimeHub: publish locks the topic, writes a realtime_events row and NOTIFYs (in the publisher's transaction when given), every replica LISTENs and fans out, replay reads the table, and a lost LISTEN connection is reloaded per topic (both editions)."
|
|
555
|
+
spec: src/modules/platform/realtime/postgres-realtime-hub.service.spec.ts
|
|
556
|
+
optional: true
|
|
557
|
+
- path: src/modules/platform/realtime/redis-realtime-hub.service.ts
|
|
558
|
+
slot: be.platform
|
|
559
|
+
role: "RedisRealtimeHub: a Redis Stream per topic for replay and Pub/Sub for the live push, published by one atomic script over ioredis, reloaded per topic after a subscriber reconnect (when Redis is declared)."
|
|
560
|
+
spec: src/modules/platform/realtime/redis-realtime-hub.service.spec.ts
|
|
561
|
+
optional: true
|
|
562
|
+
- path: src/modules/platform/realtime/realtime-subscriptions.policy.ts
|
|
563
|
+
slot: be.platform
|
|
564
|
+
role: "The subscriptions of one replica, shared by the shared hubs: join, replay then live without a duplicate, release on stop."
|
|
565
|
+
spec: src/modules/platform/realtime/realtime-subscriptions.policy.spec.ts
|
|
566
|
+
optional: true
|
|
567
|
+
- path: src/modules/platform/realtime/postgres-listener.client.ts
|
|
568
|
+
slot: be.platform
|
|
569
|
+
role: "The LISTEN connection of the Postgres hub: proves itself with a probe at boot, reconnects, closes the dead client and tells the hub it was lost and when it resumed."
|
|
570
|
+
spec: src/modules/platform/realtime/postgres-listener.client.spec.ts
|
|
571
|
+
optional: true
|
|
572
|
+
- path: src/modules/platform/realtime/realtime-retention.service.ts
|
|
309
573
|
slot: be.platform
|
|
310
|
-
role:
|
|
311
|
-
spec: src/modules/platform/realtime/realtime.service.spec.ts
|
|
574
|
+
role: The bounded, batched delete of the replay table that the fenced sweep job runs, asking the fence before every batch.
|
|
575
|
+
spec: src/modules/platform/realtime/realtime-retention.service.spec.ts
|
|
576
|
+
optional: true
|
|
577
|
+
- path: src/modules/platform/realtime/no-realtime-retention.service.ts
|
|
578
|
+
slot: be.platform
|
|
579
|
+
role: "The retention of a hub with no replay table (memory, Redis): a scheduled sweep tick is acknowledged and deletes nothing."
|
|
580
|
+
spec: src/modules/platform/realtime/no-realtime-retention.service.spec.ts
|
|
581
|
+
optional: true
|
|
582
|
+
- path: src/modules/platform/realtime/redis-stream-id.policy.ts
|
|
583
|
+
slot: be.platform
|
|
584
|
+
role: Orders two Redis stream ids, so the Redis hub reloads only what a topic has not delivered.
|
|
585
|
+
spec: src/modules/platform/realtime/redis-stream-id.policy.spec.ts
|
|
586
|
+
optional: true
|
|
587
|
+
- path: src/modules/platform/realtime/postgres-io.adapter.ts
|
|
588
|
+
slot: be.platform
|
|
589
|
+
role: "PostgresIoAdapter: socket.io over @socket.io/postgres-adapter, websocket only, so no sticky session."
|
|
590
|
+
spec: src/modules/platform/realtime/postgres-io.adapter.spec.ts
|
|
591
|
+
optional: true
|
|
592
|
+
- path: src/modules/platform/realtime/redis-io.adapter.ts
|
|
593
|
+
slot: be.platform
|
|
594
|
+
role: "RedisIoAdapter: socket.io over the sharded @socket.io/redis-adapter (ioredis), websocket only."
|
|
595
|
+
spec: src/modules/platform/realtime/redis-io.adapter.spec.ts
|
|
596
|
+
optional: true
|
|
597
|
+
- path: src/modules/platform/realtime/realtime-io.adapter.ts
|
|
598
|
+
slot: be.platform
|
|
599
|
+
role: "realtimeIoAdapter(app), the one call main.ts makes to install the socket.io adapter the selected hub provides."
|
|
600
|
+
spec: src/modules/platform/realtime/realtime-io.adapter.spec.ts
|
|
312
601
|
optional: true
|
|
313
602
|
- path: src/modules/platform/realtime/event-stream.mapper.ts
|
|
314
603
|
slot: be.platform
|
|
315
604
|
role: eventStream, the one place that turns hub frames into server-sent events (opening frame, ids, comment heartbeat) and owns the heartbeat timer.
|
|
316
605
|
spec: src/modules/platform/realtime/event-stream.mapper.spec.ts
|
|
317
606
|
optional: true
|
|
607
|
+
- path: src/modules/platform/realtime/persistence/migrations/<epochMs13>-create-realtime-events.ts
|
|
608
|
+
slot: be.persistence
|
|
609
|
+
role: Full edition migration of the realtime_events replay table and the socket_io_attachments table of the Postgres adapter.
|
|
610
|
+
optional: true
|
|
611
|
+
- path: supabase/migrations/<timestamp>_realtime-events.sql
|
|
612
|
+
slot: repo.side-root-forbidden
|
|
613
|
+
role: Lite edition migration (created with supabase migration new) of the same two tables, forced row level security for app_be, and the pg_cron tick of the retention queue.
|
|
614
|
+
optional: true
|
|
318
615
|
- path: src/features/reactors/<name>/transport/message/<event>.consumer.ts
|
|
319
616
|
slot: be.transport.message
|
|
320
617
|
role: The reactor consumer that receives the domain event and pushes the frame through the hub.
|
|
@@ -325,24 +622,26 @@ files:
|
|
|
325
622
|
spec: realtime/subscriber-receives-push, realtime/foreign-topic-is-isolated, realtime/disconnect-releases-subscription
|
|
326
623
|
rules: [BE_PATTERN_SPEC_MISSING]
|
|
327
624
|
optional: true
|
|
328
|
-
# Edition lite (R212, owner ruling 2026-10-14; the SSE door by owner decision 2026-10-24, ruling:R212/case-5): the same ports,
|
|
625
|
+
# Edition lite (R212, owner ruling 2026-10-14; the SSE door by owner decision 2026-10-24, ruling:R212/case-5; the GraphQL subscription door, ruling:R212/case-7 and owner:lite-graphql-subscription-door): the same ports, doors and rules; it scales horizontally and stateless.
|
|
329
626
|
liteAdapter:
|
|
330
627
|
summary: >-
|
|
331
|
-
Under lite the realtime pattern is the full one with its
|
|
332
|
-
(transport/sse/<channel>.controller.ts, the default for one-way push)
|
|
333
|
-
(transport/websocket/<channel>.gateway.ts) runs inside the be api process
|
|
334
|
-
|
|
628
|
+
Under lite the realtime pattern is the full one with its three doors: the SSE controller of be.feature.realtime.sse
|
|
629
|
+
(transport/sse/<channel>.controller.ts, the default for one-way push), the socket.io gateway of be.feature.realtime.websocket
|
|
630
|
+
(transport/websocket/<channel>.gateway.ts) or the GraphQL subscription of be.feature.realtime.graphql (restricted, BE-REALTIME-6) runs inside the be api process. One replica or a test runs the memory hub (its bounded replay buffer and the socket.io
|
|
631
|
+
default adapter); replicas > 1 run the Redis hub and the sharded @socket.io/redis-adapter when Redis is declared, else the Postgres hub
|
|
632
|
+
(LISTEN/NOTIFY and the bounded realtime_events table) and @socket.io/postgres-adapter, websocket only, with no sticky sessions (BE-REALTIME-7). A publisher (a reactor, or the domain service that
|
|
335
633
|
commits the change in an app with no event bus) publishes to the RealtimeHub of platform/realtime; the SSE door streams the
|
|
336
|
-
principal-scoped topic as text/event-stream
|
|
337
|
-
listens through a
|
|
338
|
-
|
|
634
|
+
principal-scoped topic as text/event-stream, the gateway emits to the principal-scoped room of the namespace and the subscription resolver
|
|
635
|
+
returns the hub stream of the principal-scoped topic. The front end listens through a useSubscribe<Channel>Swr hook over modules/api/sse
|
|
636
|
+
(a reader of the transport client layer with the bearer token) or hooks/socketio (knowledge/patterns/fe/realtime.yaml). graphql-ws
|
|
637
|
+
has no replay, so a screen on the subscription refetches its snapshot on reconnect. The memory hub serves tests and one replica; with more than one api replica the hub is the Redis hub when Redis is declared, else the Postgres hub (stateless, no sticky sessions; BE-REALTIME-7).
|
|
339
638
|
ports:
|
|
340
|
-
- {port: RealtimeHub.publish (platform/realtime), adapter: "the memory hub
|
|
639
|
+
- {port: RealtimeHub.publish (platform/realtime), adapter: "the memory hub (one replica, tests) retains the frame (bounded) in the api process; the Postgres hub (replicas > 1) writes the realtime_events row and NOTIFYs, every replica hands it to its open SSE streams; the socket.io server emits to the namespace room of the topic, across replicas through @socket.io/postgres-adapter (websocket only)"}
|
|
341
640
|
- {port: RealtimeHub.subscribe, adapter: "the SSE controller streams hub.stream(topic, { after }) (the subscription through eventStream); the gateway's stream joins the room built from the principal, as full"}
|
|
342
641
|
- {port: topic scope, adapter: "the topic and the room name built from @CurrentPrincipal (BE_REALTIME_TOPIC_SCOPE), as full"}
|
|
343
|
-
forbids: [
|
|
642
|
+
forbids: []
|
|
344
643
|
proof: >-
|
|
345
644
|
The door's own unit spec (<channel>.controller.spec.ts or <channel>.gateway.spec.ts) and the hub adapter's spec, as full;
|
|
346
645
|
declaring realtime requires integration specs titled `realtime/<scenario>: ...` for every declared scenario on
|
|
347
646
|
`starci supabase start` (R284), using a real client (an HTTP event-stream read or a socket.io client). Lite has no e2e world.
|
|
348
|
-
see: [knowledge/patterns/
|
|
647
|
+
see: [knowledge/patterns/fe/realtime.yaml]
|
|
@@ -147,7 +147,7 @@ files:
|
|
|
147
147
|
optional: true
|
|
148
148
|
- path: src/modules/platform/saga/
|
|
149
149
|
slot: be.platform
|
|
150
|
-
role: 'The
|
|
150
|
+
role: 'The fenced `saga_states` store and the state machine in `saga.service.ts`, which claims deliveries through the shared `platform/inbox` port (BE-IDEMPOTENCY-2). The service is a measured file of a `coverage: required` slot, held at 100 per file with its spec beside it.'
|
|
151
151
|
rules: [R170, R80]
|
|
152
152
|
optional: true
|
|
153
153
|
- path: src/tests/e2e/<area>/<name>.e2e-spec.ts
|
|
@@ -275,4 +275,4 @@ liteAdapter:
|
|
|
275
275
|
The orchestrator's unit spec (<saga>.saga.service.spec.ts) as full; declaring saga requires integration specs titled
|
|
276
276
|
`saga/<scenario>: ...` for every declared scenario on `starci supabase start` (R284), including each compensation path with
|
|
277
277
|
failure injection and the final state read back. The e2e world of R173 remains full-only.
|
|
278
|
-
see: [knowledge/patterns/be/
|
|
278
|
+
see: [knowledge/patterns/be/event-bus.yaml]
|
|
@@ -129,7 +129,7 @@ enforces:
|
|
|
129
129
|
- {rule: R21, code: HFS_DUPLICATE_CODE, enforcer: duplicated-lines-density, condition: duplication}
|
|
130
130
|
- {rule: R21, code: HFS_DUPLICATE_SYMBOL, enforcer: duplicated-lines-density, condition: duplication}
|
|
131
131
|
# Ops that write product code: each runs sonar-local on its own change and cannot settle done while the gate is red.
|
|
132
|
-
enforcedOps: [backend.implement, interface.implement, code.refactor]
|
|
132
|
+
enforcedOps: [backend.implement, interface.implement, code.refactor, code.fix]
|
|
133
133
|
reasoning: |
|
|
134
134
|
Chosen against the four projects the local server holds on 2026-09-29 (whole-project duplicated lines
|
|
135
135
|
1.6 / 2.1 / 1.7 / 0.0 percent), so new code that meets them is at or below what main already carries.
|