@a5c-ai/kradle 5.0.1-staging.3abdf9534c25
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/Dockerfile +31 -0
- package/README.md +187 -0
- package/bin/kradle-demo.mjs +23 -0
- package/bin/kradle-server.mjs +14 -0
- package/dist/kradle-controller-ui.json +3482 -0
- package/dist/kradle-lifecycle.json +201 -0
- package/dist/kradle-runtime-snapshot.json +3125 -0
- package/dist/kradle-summary.json +724 -0
- package/docs/README.md +61 -0
- package/docs/agents/README.md +83 -0
- package/docs/agents/acceptance-test-matrix.md +193 -0
- package/docs/agents/agent-mux-adapter-contract.md +167 -0
- package/docs/agents/agent-mux-source-map.md +310 -0
- package/docs/agents/agent-run-memory-import-spec.md +256 -0
- package/docs/agents/agent-stack-management-spec.md +421 -0
- package/docs/agents/api-contract-spec.md +309 -0
- package/docs/agents/artifacts-writeback-spec.md +145 -0
- package/docs/agents/chart-packaging-spec.md +128 -0
- package/docs/agents/ci-orchestration-spec.md +140 -0
- package/docs/agents/context-assembly-spec.md +219 -0
- package/docs/agents/controller-reconciliation-spec.md +255 -0
- package/docs/agents/crd-schema-spec.md +315 -0
- package/docs/agents/decision-log-open-questions.md +169 -0
- package/docs/agents/developer-implementation-checklist.md +329 -0
- package/docs/agents/dispatching-design.md +262 -0
- package/docs/agents/gaps-agent-mux-to-kradle-crds.md +298 -0
- package/docs/agents/glossary.md +66 -0
- package/docs/agents/implementation-blueprint.md +324 -0
- package/docs/agents/implementation-rollout-slices.md +251 -0
- package/docs/agents/memory-context-integration-spec.md +194 -0
- package/docs/agents/memory-ontology-schema-spec.md +253 -0
- package/docs/agents/memory-operations-runbook.md +121 -0
- package/docs/agents/mvp-vertical-slice-spec.md +146 -0
- package/docs/agents/observability-audit-spec.md +265 -0
- package/docs/agents/operator-runbook.md +174 -0
- package/docs/agents/org-memory-api-payload-examples.md +333 -0
- package/docs/agents/org-memory-controller-sequence-spec.md +181 -0
- package/docs/agents/org-memory-e2e-fixture-plan.md +161 -0
- package/docs/agents/org-memory-ui-implementation-map.md +114 -0
- package/docs/agents/org-memory-vertical-slice-spec.md +168 -0
- package/docs/agents/org-resource-model-delta-spec.md +111 -0
- package/docs/agents/org-route-resource-model-spec.md +183 -0
- package/docs/agents/org-scoping-namespace-spec.md +114 -0
- package/docs/agents/rbac-secrets-management-spec.md +406 -0
- package/docs/agents/repository-page-integration-spec.md +255 -0
- package/docs/agents/resource-contract-examples.md +808 -0
- package/docs/agents/resource-relationship-map.md +190 -0
- package/docs/agents/security-threat-model.md +188 -0
- package/docs/agents/shared-memory-company-brain-spec.md +358 -0
- package/docs/agents/storage-migration-spec.md +168 -0
- package/docs/agents/subagent-orchestration-spec.md +152 -0
- package/docs/agents/system-overview.md +88 -0
- package/docs/agents/tools-mcp-skills-spec.md +189 -0
- package/docs/agents/traceability-matrix.md +79 -0
- package/docs/agents/ui-flow-spec.md +211 -0
- package/docs/agents/ui-ux-system-spec.md +426 -0
- package/docs/agents/workspace-lifecycle-spec.md +166 -0
- package/docs/architecture-spec.md +78 -0
- package/docs/architecture-v2.md +2759 -0
- package/docs/components/control-plane.md +78 -0
- package/docs/components/data-plane.md +69 -0
- package/docs/components/hooks-events.md +67 -0
- package/docs/components/identity-rbac-policy.md +73 -0
- package/docs/components/kubevela-oam.md +70 -0
- package/docs/components/operations-publishing.md +81 -0
- package/docs/components/runners-ci.md +66 -0
- package/docs/components/web-ui.md +94 -0
- package/docs/crd-behaviors-and-relationships.md +3926 -0
- package/docs/external/README.md +47 -0
- package/docs/external/bidirectional-sync-design.md +134 -0
- package/docs/external/cicd-interface.md +64 -0
- package/docs/external/external-backend-controllers.md +170 -0
- package/docs/external/external-backend-crds.md +234 -0
- package/docs/external/external-backend-ui-spec.md +151 -0
- package/docs/external/external-backend-ux-flows.md +115 -0
- package/docs/external/external-object-mapping.md +125 -0
- package/docs/external/git-forge-interface.md +68 -0
- package/docs/external/github-integration-design.md +151 -0
- package/docs/external/issue-tracking-interface.md +66 -0
- package/docs/external/provider-capability-manifests.md +204 -0
- package/docs/external/provider-catalog.md +139 -0
- package/docs/external/provider-rollout-testing.md +78 -0
- package/docs/external/research-results.md +48 -0
- package/docs/external/security-auth-permissions.md +81 -0
- package/docs/external/sync-state-machines.md +108 -0
- package/docs/external/unified-external-backend-model.md +107 -0
- package/docs/external/user-facing-changes.md +67 -0
- package/docs/gaps.md +161 -0
- package/docs/install.md +94 -0
- package/docs/integration-and-design-decisions.md +1530 -0
- package/docs/kradle-design.md +334 -0
- package/docs/local-minikube.md +55 -0
- package/docs/ontology/README.md +32 -0
- package/docs/ontology/bounded-contexts.md +29 -0
- package/docs/ontology/events-and-hooks.md +32 -0
- package/docs/ontology/oam-kubevela.md +32 -0
- package/docs/ontology/operations-and-release.md +25 -0
- package/docs/ontology/personas-and-actors.md +32 -0
- package/docs/ontology/policies-and-invariants.md +33 -0
- package/docs/ontology/problem-space.md +30 -0
- package/docs/ontology/resource-contracts.md +40 -0
- package/docs/ontology/resource-taxonomy.md +42 -0
- package/docs/ontology/runners-and-ci.md +29 -0
- package/docs/ontology/solution-space.md +24 -0
- package/docs/ontology/storage-and-data-boundaries.md +29 -0
- package/docs/ontology/validation-matrix.md +24 -0
- package/docs/ontology/web-ui-excellent-flows.md +32 -0
- package/docs/ontology/workflows.md +39 -0
- package/docs/ontology/world.md +35 -0
- package/docs/openapi.yaml +1291 -0
- package/docs/product-requirements.md +62 -0
- package/docs/requirements-v2.md +235 -0
- package/docs/roadmap-mvp.md +87 -0
- package/docs/sdk-api-reference.md +1108 -0
- package/docs/system-requirements.md +90 -0
- package/docs/system-spec-v2.md +1230 -0
- package/docs/tests/README.md +53 -0
- package/docs/tests/agent-qa-plan.md +63 -0
- package/docs/tests/browser-ui-tests.md +62 -0
- package/docs/tests/ci-quality-gates.md +48 -0
- package/docs/tests/coverage-model.md +64 -0
- package/docs/tests/e2e-scenario-tests.md +53 -0
- package/docs/tests/fixtures-test-data.md +63 -0
- package/docs/tests/observability-reliability-tests.md +54 -0
- package/docs/tests/product-test-matrix.md +145 -0
- package/docs/tests/qa-adoption-roadmap.md +130 -0
- package/docs/tests/qa-automation-plan.md +101 -0
- package/docs/tests/security-compliance-tests.md +57 -0
- package/docs/tests/test-framework-tools.md +88 -0
- package/docs/tests/test-suite-layout.md +121 -0
- package/docs/tests/unit-integration-tests.md +48 -0
- package/docs/todo-kyverno +714 -0
- package/docs/todos.md +4 -0
- package/docs/user-stories.md +78 -0
- package/docs/web-console-spec.md +533 -0
- package/examples/minikube-demo.yaml +190 -0
- package/examples/oam-application.yaml +23 -0
- package/examples/policy-kyverno-pr-title.yaml +18 -0
- package/package.json +66 -0
- package/scripts/build.mjs +29 -0
- package/scripts/setup-minikube.mjs +65 -0
- package/scripts/smoke.mjs +37 -0
- package/scripts/validate-doc-coverage.mjs +152 -0
- package/scripts/validate-package.mjs +95 -0
- package/scripts/validate-ui.mjs +305 -0
- package/src/agent-adapter-controller.js +169 -0
- package/src/agent-approval-controller.js +170 -0
- package/src/agent-context-bundles.js +242 -0
- package/src/agent-dispatch-controller.js +549 -0
- package/src/agent-gateway-config-controller.js +147 -0
- package/src/agent-identity-migration.js +115 -0
- package/src/agent-memory-controller.js +357 -0
- package/src/agent-memory-import.js +327 -0
- package/src/agent-memory-query.js +292 -0
- package/src/agent-memory-repository-source-controller.js +255 -0
- package/src/agent-mux-client.js +589 -0
- package/src/agent-permission-review.js +250 -0
- package/src/agent-persona-controller.js +135 -0
- package/src/agent-project-controller.js +117 -0
- package/src/agent-prompt-composition.js +55 -0
- package/src/agent-provider-config-controller.js +151 -0
- package/src/agent-secret-config-grant-controller.js +282 -0
- package/src/agent-session-transcript-controller.js +189 -0
- package/src/agent-stack-controller.js +421 -0
- package/src/agent-subagent-controller.js +160 -0
- package/src/agent-transport-binding-controller.js +121 -0
- package/src/agent-trigger-controller.js +387 -0
- package/src/agent-workspace-controller.js +702 -0
- package/src/agent-writeback-controller.js +302 -0
- package/src/api-controller.js +621 -0
- package/src/argocd-gitops.js +43 -0
- package/src/artifact-registry-controller.js +542 -0
- package/src/assistant-runtime.js +284 -0
- package/src/async-controller.js +207 -0
- package/src/audit-controller.js +191 -0
- package/src/auth.js +310 -0
- package/src/component-catalog.js +41 -0
- package/src/control-plane.js +136 -0
- package/src/controller-client.js +112 -0
- package/src/controller-ui.js +620 -0
- package/src/data-plane.js +179 -0
- package/src/event-bus.js +397 -0
- package/src/external/conflict-controller.js +225 -0
- package/src/external/github/auth.js +96 -0
- package/src/external/github/cicd.js +180 -0
- package/src/external/github/git-forge.js +240 -0
- package/src/external/github/index.js +144 -0
- package/src/external/github/issue-tracking.js +163 -0
- package/src/external/provider-adapter.js +161 -0
- package/src/external/provider-resource-factory.js +221 -0
- package/src/external/sync-controller.js +235 -0
- package/src/external/webhook-controller.js +144 -0
- package/src/external/write-controller.js +283 -0
- package/src/gitea-backend.js +131 -0
- package/src/gitea-service.js +173 -0
- package/src/handoff.js +98 -0
- package/src/health-probes.js +134 -0
- package/src/hooks-events.js +63 -0
- package/src/hooks-lifecycle.js +117 -0
- package/src/http-server.js +409 -0
- package/src/identity-policy.js +86 -0
- package/src/index.js +71 -0
- package/src/jitsi-agent-bridge.js +141 -0
- package/src/jitsi-meeting-controller.js +291 -0
- package/src/jitsi-sync-controller.js +198 -0
- package/src/kradle-inference-service-controller.js +246 -0
- package/src/kubernetes-controller-async.js +531 -0
- package/src/kubernetes-controller.js +904 -0
- package/src/kubernetes-resource-gateway.js +48 -0
- package/src/model-route-controller.js +364 -0
- package/src/notification-controller.js +178 -0
- package/src/operations.js +112 -0
- package/src/org-scoping.js +5 -0
- package/src/resource-model.js +282 -0
- package/src/runner-controller.js +272 -0
- package/src/runners-ci.js +48 -0
- package/src/runtime.js +196 -0
- package/src/snapshot-cache.js +157 -0
- package/src/virtual-model-controller.js +538 -0
- package/src/virtual-model-hook-bridge.js +200 -0
- package/src/web-ui.js +40 -0
- package/tests/agent-adapter-controller.test.js +361 -0
- package/tests/agent-approval-controller.test.js +173 -0
- package/tests/agent-context-bundles.test.js +278 -0
- package/tests/agent-dispatch-controller.test.js +679 -0
- package/tests/agent-gateway-config-controller.test.js +386 -0
- package/tests/agent-identity-migration.test.js +87 -0
- package/tests/agent-memory-controller.test.js +461 -0
- package/tests/agent-memory-import-snapshot.test.js +477 -0
- package/tests/agent-memory-query.test.js +404 -0
- package/tests/agent-memory-repository-source.test.js +514 -0
- package/tests/agent-mux-client.test.js +389 -0
- package/tests/agent-mux-integration.test.js +971 -0
- package/tests/agent-permission-review-v2.test.js +317 -0
- package/tests/agent-permission-review.test.js +209 -0
- package/tests/agent-persona-controller.test.js +127 -0
- package/tests/agent-project-controller.test.js +302 -0
- package/tests/agent-prompt-composition.test.js +76 -0
- package/tests/agent-provider-config-controller.test.js +376 -0
- package/tests/agent-resources.test.js +303 -0
- package/tests/agent-secret-config-grant.test.js +231 -0
- package/tests/agent-session-transcript-controller.test.js +499 -0
- package/tests/agent-stack-controller.test.js +283 -0
- package/tests/agent-subagent-controller.test.js +201 -0
- package/tests/agent-transport-binding-controller.test.js +294 -0
- package/tests/agent-trigger-controller.test.js +271 -0
- package/tests/agent-trigger-routes.test.js +190 -0
- package/tests/agent-trigger-sources.test.js +245 -0
- package/tests/agent-workspace-controller.test.js +181 -0
- package/tests/agent-writeback.test.js +292 -0
- package/tests/approval-persistence.test.js +171 -0
- package/tests/artifact-registry.test.js +511 -0
- package/tests/assistant-runtime.test.js +506 -0
- package/tests/async-controller.test.js +252 -0
- package/tests/audit-controller.test.js +227 -0
- package/tests/codespace-controller.test.js +318 -0
- package/tests/controller-client.test.js +133 -0
- package/tests/deployment.test.js +527 -0
- package/tests/e2e/lifecycle.test.js +120 -0
- package/tests/event-bus-integration.test.js +355 -0
- package/tests/external-github-forge.test.js +560 -0
- package/tests/external-github-issues-cicd.test.js +520 -0
- package/tests/external-integration.test.js +470 -0
- package/tests/external-persistence.test.js +415 -0
- package/tests/external-provider-adapter.test.js +365 -0
- package/tests/external-resource-model.test.js +223 -0
- package/tests/external-webhook-sync.test.js +287 -0
- package/tests/external-write-conflict.test.js +353 -0
- package/tests/gitea-service.test.js +253 -0
- package/tests/health-check-real.test.js +165 -0
- package/tests/health-probes.test.js +90 -0
- package/tests/hooks-lifecycle.test.js +364 -0
- package/tests/integration/full-flow.test.js +266 -0
- package/tests/jitsi-agent-bridge.test.js +119 -0
- package/tests/jitsi-helm-integration.test.js +77 -0
- package/tests/jitsi-meeting-controller.test.js +170 -0
- package/tests/jitsi-resource-model.test.js +73 -0
- package/tests/jitsi-sync-controller.test.js +112 -0
- package/tests/kradle-inference-service.test.js +689 -0
- package/tests/kradle.test.js +779 -0
- package/tests/memory-search-wiring.test.js +270 -0
- package/tests/model-route-controller.test.js +733 -0
- package/tests/notification-controller.test.js +196 -0
- package/tests/notification-integration.test.js +179 -0
- package/tests/org-scoping.test.js +687 -0
- package/tests/runner-controller.test.js +327 -0
- package/tests/runner-integration.test.js +231 -0
- package/tests/session-cookie-hmac.test.js +151 -0
- package/tests/snapshot-performance.test.js +315 -0
- package/tests/sse-events.test.js +107 -0
- package/tests/virtual-model-controller.test.js +877 -0
- package/tests/virtual-model-hook-bridge.test.js +384 -0
- package/tests/webhook-trigger.test.js +198 -0
- package/tests/workspace-volumes.test.js +312 -0
- package/tests/writeback-persistence.test.js +207 -0
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { giteaIssueSyncPlan, giteaRepositoryIntegrationPlan, orgMemoryRepositoryName } from './gitea-backend.js';
|
|
2
|
+
import { clone, createResource } from './resource-model.js';
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
export function createDefaultGiteaGitBackend({ baseUrl = 'http://kradle-gitea-http:3000', sshDomain = 'kradle-gitea-ssh', owner = 'kradle', webhookBaseUrl = 'http://kradle-webhook-worker' } = {}) {
|
|
6
|
+
return { type: 'gitea', baseUrl: baseUrl.replace(/\/$/, ''), sshDomain, owner, webhookBaseUrl: webhookBaseUrl.replace(/\/$/, '') };
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function createGiteaRepositoryHosting({ backend = createDefaultGiteaGitBackend(), namespace = 'default', repository, branch = 'main' }) {
|
|
10
|
+
const owner = backend.owner || namespace;
|
|
11
|
+
const httpUrl = `${backend.baseUrl}/${owner}/${repository}.git`;
|
|
12
|
+
const sshUrl = `ssh://git@${backend.sshDomain}/${owner}/${repository}.git`;
|
|
13
|
+
const webhookUrl = `${backend.webhookBaseUrl}/repositories/${namespace}/${repository}`;
|
|
14
|
+
return {
|
|
15
|
+
backend: 'gitea',
|
|
16
|
+
owner,
|
|
17
|
+
repository,
|
|
18
|
+
branch,
|
|
19
|
+
httpUrl,
|
|
20
|
+
sshUrl,
|
|
21
|
+
deployKeyTitle: 'kradle-argocd',
|
|
22
|
+
organization: { kind: 'Organization', name: owner, delegatedTo: 'Gitea /api/v1/orgs' },
|
|
23
|
+
sshKeys: { kind: 'SSHKey', scopes: ['user', 'deploy', 'argocd'], delegatedTo: 'Gitea /api/v1/user/keys and /repos/{owner}/{repo}/keys' },
|
|
24
|
+
permissions: { kind: 'RepositoryPermission', defaultCollaborator: 'write', adminTeam: 'maintainers', delegatedTo: 'Gitea collaborators and team repository APIs' },
|
|
25
|
+
forgeRecords: { issues: `Gitea /repos/${owner}/${orgMemoryRepositoryName(namespace)}/issues`, pullRequests: 'Gitea /repos/{owner}/{repo}/pulls' },
|
|
26
|
+
issueSync: giteaIssueSyncPlan({ org: namespace, repositories: [repository] }),
|
|
27
|
+
webhookUrl,
|
|
28
|
+
integrationPlan: giteaRepositoryIntegrationPlan({ owner, repo: repository, deployKeyTitle: 'kradle-argocd', permission: 'write', branch, webhookUrl })
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function stableRepositoryStoreIndex(repositoryName, storeCount) {
|
|
33
|
+
let hash = 0;
|
|
34
|
+
for (const character of repositoryName) hash = (hash * 31 + character.charCodeAt(0)) >>> 0;
|
|
35
|
+
return hash % storeCount;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export class GiteaRepositoryStore {
|
|
39
|
+
constructor({ name = 'gitea-primary', receivePackReady = true, capacity = 1000 } = {}) {
|
|
40
|
+
this.name = name;
|
|
41
|
+
this.receivePackReady = receivePackReady;
|
|
42
|
+
this.capacity = capacity;
|
|
43
|
+
this.repositories = new Map();
|
|
44
|
+
this.objects = new Map();
|
|
45
|
+
this.searchIndex = new Map();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
assign(repository) { this.repositories.set(repository.metadata.name, repository); return this; }
|
|
49
|
+
isReceivePackReady() { return this.receivePackReady; }
|
|
50
|
+
|
|
51
|
+
putObject(repository, object) {
|
|
52
|
+
const objects = this.objects.get(repository) || [];
|
|
53
|
+
objects.push(object);
|
|
54
|
+
this.objects.set(repository, objects);
|
|
55
|
+
return object;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
index(repository, entry) {
|
|
59
|
+
const entries = this.searchIndex.get(repository) || [];
|
|
60
|
+
entries.push(entry);
|
|
61
|
+
this.searchIndex.set(repository, entries);
|
|
62
|
+
return entry;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
snapshot() {
|
|
66
|
+
return {
|
|
67
|
+
name: this.name,
|
|
68
|
+
receivePackReady: this.receivePackReady,
|
|
69
|
+
capacity: this.capacity,
|
|
70
|
+
repositories: Object.fromEntries([...this.repositories.entries()].map(([name, resource]) => [name, clone(resource)])),
|
|
71
|
+
objects: Object.fromEntries([...this.objects.entries()].map(([repository, objects]) => [repository, clone(objects)])),
|
|
72
|
+
searchIndex: Object.fromEntries([...this.searchIndex.entries()].map(([repository, entries]) => [repository, clone(entries)]))
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
importSnapshot(snapshot = {}) {
|
|
77
|
+
this.receivePackReady = snapshot.receivePackReady ?? this.receivePackReady;
|
|
78
|
+
this.capacity = snapshot.capacity ?? this.capacity;
|
|
79
|
+
this.repositories = new Map(Object.entries(snapshot.repositories || {}).map(([name, resource]) => [name, clone(resource)]));
|
|
80
|
+
this.objects = new Map(Object.entries(snapshot.objects || {}).map(([repository, objects]) => [repository, clone(objects)]));
|
|
81
|
+
this.searchIndex = new Map(Object.entries(snapshot.searchIndex || {}).map(([repository, entries]) => [repository, clone(entries)]));
|
|
82
|
+
return this;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export class GiteaGitService {
|
|
87
|
+
constructor({ controlPlane, stores = [new GiteaRepositoryStore()], gitBackend = createDefaultGiteaGitBackend() }) {
|
|
88
|
+
if (!controlPlane) throw new Error('GiteaGitService requires a controlPlane');
|
|
89
|
+
this.controlPlane = controlPlane;
|
|
90
|
+
this.stores = stores;
|
|
91
|
+
this.gitBackend = gitBackend.type === 'gitea' ? gitBackend : createDefaultGiteaGitBackend(gitBackend);
|
|
92
|
+
this.integrationPlans = new Map();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
snapshot() {
|
|
96
|
+
return {
|
|
97
|
+
apiVersion: 'kradle.a5c.ai/v1alpha1',
|
|
98
|
+
kind: 'GiteaGitServiceSnapshot',
|
|
99
|
+
backend: clone(this.gitBackend),
|
|
100
|
+
integrationPlans: Object.fromEntries([...this.integrationPlans.entries()].map(([repository, plan]) => [repository, clone(plan)])),
|
|
101
|
+
stores: this.stores.map((store) => store.snapshot())
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
importSnapshot(snapshot = {}) {
|
|
106
|
+
if (snapshot.backend?.type === 'gitea') this.gitBackend = clone(snapshot.backend);
|
|
107
|
+
this.integrationPlans = new Map(Object.entries(snapshot.integrationPlans || {}).map(([repository, plan]) => [repository, clone(plan)]));
|
|
108
|
+
if (!Array.isArray(snapshot.stores)) return this;
|
|
109
|
+
this.stores = snapshot.stores.map((storeSnapshot) => new GiteaRepositoryStore({
|
|
110
|
+
name: storeSnapshot.name,
|
|
111
|
+
receivePackReady: storeSnapshot.receivePackReady,
|
|
112
|
+
capacity: storeSnapshot.capacity
|
|
113
|
+
}).importSnapshot(storeSnapshot));
|
|
114
|
+
return this;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
route(repositoryName) {
|
|
118
|
+
const store = this.stores[stableRepositoryStoreIndex(repositoryName, this.stores.length)];
|
|
119
|
+
const owner = this.gitBackend.owner || 'kradle';
|
|
120
|
+
return { repositoryName, backend: 'gitea', owner, store: store.name, receivePackReady: store.isReceivePackReady(), httpUrl: `${this.gitBackend.baseUrl}/${owner}/${repositoryName}.git`, sshUrl: `ssh://git@${this.gitBackend.sshDomain}/${owner}/${repositoryName}.git` };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
createRepository({ name, namespace = 'kradle-org-default', organizationRef = 'default', visibility = 'private' }, user) {
|
|
124
|
+
const route = this.route(name);
|
|
125
|
+
const hosting = createGiteaRepositoryHosting({ backend: this.gitBackend, namespace, repository: name });
|
|
126
|
+
const repository = createResource('Repository', { name, namespace, labels: { gitBackend: 'gitea' } }, {
|
|
127
|
+
organizationRef,
|
|
128
|
+
visibility,
|
|
129
|
+
gitHosting: hosting,
|
|
130
|
+
storage: { mode: 'gitea', persistentVolumeClaim: 'kradle-gitea-data', owner: hosting.owner, repository: name, httpUrl: hosting.httpUrl, sshUrl: hosting.sshUrl },
|
|
131
|
+
objectStorage: { lfs: true, artifacts: true },
|
|
132
|
+
search: { provider: 'zoekt', enabled: false }
|
|
133
|
+
}, { ready: true, route: { ...route, backend: 'gitea' }, gitHosting: { backend: 'gitea', httpUrl: hosting.httpUrl, sshUrl: hosting.sshUrl, integrationPlan: hosting.integrationPlan } });
|
|
134
|
+
const created = this.controlPlane.create(repository, user);
|
|
135
|
+
this.integrationPlans.set(name, hosting.integrationPlan);
|
|
136
|
+
this.stores.find((store) => store.name === route.store).assign(created);
|
|
137
|
+
return created;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
receivePack({ repository, namespace = 'kradle-org-default', ref, oldRev = '0'.repeat(40), newRev, actor }) {
|
|
141
|
+
const route = this.route(repository);
|
|
142
|
+
if (!route.receivePackReady) throw new Error(`receive-pack for ${repository} is not ready`);
|
|
143
|
+
const refPolicies = this.controlPlane.list('RefPolicy', { namespace }).items;
|
|
144
|
+
const branchProtections = this.controlPlane.list('BranchProtection', { namespace }).items;
|
|
145
|
+
const deniedByPolicy = refPolicies.find((policy) => (policy.spec.deny || []).some((prefix) => ref.startsWith(prefix)));
|
|
146
|
+
if (deniedByPolicy) throw new Error(`RefPolicy ${deniedByPolicy.metadata.name} denied ${ref}`);
|
|
147
|
+
const protectedBranch = branchProtections.find((policy) => (policy.spec.refs || []).includes(ref));
|
|
148
|
+
const isRepoAdmin = actor.groups?.includes('kradle:repo-admins') || actor.groups?.includes('kradle:platform-engineers');
|
|
149
|
+
if (protectedBranch && protectedBranch.spec.requirePullRequest && !isRepoAdmin) {
|
|
150
|
+
throw new Error(`BranchProtection ${protectedBranch.metadata.name} requires pull request for ${ref}`);
|
|
151
|
+
}
|
|
152
|
+
const event = { repository, namespace, ref, oldRev, newRev, actor: actor.name, backend: 'gitea', store: route.store, remoteUrl: route.httpUrl, at: new Date().toISOString() };
|
|
153
|
+
this.controlPlane.events.push({ type: 'git.receive-pack', resource: event, storage: 'gitea' });
|
|
154
|
+
return event;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
uploadPack({ repository }) {
|
|
158
|
+
const route = this.route(repository);
|
|
159
|
+
return { repository, backend: 'gitea', store: route.store, remoteUrl: route.httpUrl, cacheable: true };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
recordObject({ repository, namespace = 'default', key, size, mediaType = 'application/octet-stream' }) {
|
|
163
|
+
const route = this.route(repository);
|
|
164
|
+
const store = this.stores.find((candidate) => candidate.name === route.store);
|
|
165
|
+
const object = { repository, namespace, key, size, mediaType, store: route.store, storage: 'object-storage', at: new Date().toISOString() };
|
|
166
|
+
store.putObject(repository, object);
|
|
167
|
+
this.controlPlane.events.push({ type: 'git.object-recorded', resource: object, storage: 'object-storage' });
|
|
168
|
+
return object;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
enqueueSearchIndex({ repository, namespace = 'default', commit, paths = [] }) {
|
|
172
|
+
const route = this.route(repository);
|
|
173
|
+
const store = this.stores.find((candidate) => candidate.name === route.store);
|
|
174
|
+
const entry = { repository, namespace, commit, paths, store: route.store, provider: 'zoekt', status: 'queued', at: new Date().toISOString() };
|
|
175
|
+
store.index(repository, entry);
|
|
176
|
+
this.controlPlane.events.push({ type: 'search.index-queued', resource: entry, storage: 'search-index' });
|
|
177
|
+
return entry;
|
|
178
|
+
}
|
|
179
|
+
}
|
package/src/event-bus.js
ADDED
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Module-level event bus for SSE real-event streaming.
|
|
3
|
+
* Preserves the historical synchronous API while allowing a shared durable
|
|
4
|
+
* transport underneath it.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
8
|
+
import { join } from 'node:path';
|
|
9
|
+
|
|
10
|
+
const EVENT_LOG_DIR = process.env.KRADLE_EVENT_LOG_DIR || join(process.env.HOME || '/tmp', '.kradle', 'events');
|
|
11
|
+
const MAX_EVENTS = 1000;
|
|
12
|
+
const DEFAULT_EVENT_SUBJECT = 'kradle.events';
|
|
13
|
+
const DEFAULT_NATS_STREAM = 'KRADLE_EVENTS';
|
|
14
|
+
|
|
15
|
+
function ensureLogDir() {
|
|
16
|
+
try {
|
|
17
|
+
if (!existsSync(EVENT_LOG_DIR)) mkdirSync(EVENT_LOG_DIR, { recursive: true });
|
|
18
|
+
} catch (err) {
|
|
19
|
+
console.warn('[event-bus] failed to create event log directory:', err.message);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function eventLogFile() {
|
|
24
|
+
ensureLogDir();
|
|
25
|
+
return join(EVENT_LOG_DIR, 'events.jsonl');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function persistEvent(event) {
|
|
29
|
+
try {
|
|
30
|
+
const logFile = eventLogFile();
|
|
31
|
+
appendFileSync(logFile, JSON.stringify(event) + '\n');
|
|
32
|
+
try {
|
|
33
|
+
const content = readFileSync(logFile, 'utf8');
|
|
34
|
+
const lines = content.trim().split('\n').filter(Boolean);
|
|
35
|
+
if (lines.length > MAX_EVENTS) writeFileSync(logFile, lines.slice(-MAX_EVENTS).join('\n') + '\n');
|
|
36
|
+
} catch (err) {
|
|
37
|
+
console.warn('[event-bus] failed to truncate event log:', err.message);
|
|
38
|
+
}
|
|
39
|
+
} catch (err) {
|
|
40
|
+
console.warn('[event-bus] failed to persist event:', err.message);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function loadPersistedEvents(limit = 100) {
|
|
45
|
+
try {
|
|
46
|
+
const logFile = join(EVENT_LOG_DIR, 'events.jsonl');
|
|
47
|
+
if (!existsSync(logFile)) return [];
|
|
48
|
+
const content = readFileSync(logFile, 'utf8');
|
|
49
|
+
const lines = content.trim().split('\n').filter(Boolean);
|
|
50
|
+
return lines.slice(-limit).map((line) => {
|
|
51
|
+
try { return JSON.parse(line); } catch { return null; }
|
|
52
|
+
}).filter(Boolean);
|
|
53
|
+
} catch {
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function createEventId() {
|
|
59
|
+
return `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function redactSecretText(value) {
|
|
63
|
+
return String(value || '')
|
|
64
|
+
.replace(/([a-z][a-z0-9+.-]*:\/\/)([^:@/\s]+):([^@/\s]+)@/gi, '$1[redacted]:[redacted]@')
|
|
65
|
+
.replace(/sk-ant-[A-Za-z0-9_-]+/g, '[redacted]')
|
|
66
|
+
.replace(/sk-[A-Za-z0-9_-]{12,}/g, '[redacted]')
|
|
67
|
+
.replace(/(token|password|secret|api[_-]?key)=([^&\s]+)/gi, '$1=[redacted]');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function sanitizeBrokerStatus(status) {
|
|
71
|
+
if (!status || typeof status !== 'object') return status;
|
|
72
|
+
return Object.fromEntries(Object.entries(status).map(([key, value]) => {
|
|
73
|
+
if (key === 'reason' || key === 'error' || key === 'url') return [key, redactSecretText(value)];
|
|
74
|
+
return [key, value];
|
|
75
|
+
}));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function normalizeEvent(event) {
|
|
79
|
+
const id = event?.id || createEventId();
|
|
80
|
+
const timestamp = event?.timestamp || new Date().toISOString();
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
id,
|
|
84
|
+
timestamp,
|
|
85
|
+
...(event || {}),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function annotateEvent(event, normalized) {
|
|
90
|
+
if (!event || typeof event !== 'object') return normalized;
|
|
91
|
+
if (!Object.prototype.hasOwnProperty.call(event, 'id')) {
|
|
92
|
+
Object.defineProperty(event, 'id', { value: normalized.id, enumerable: false, configurable: true });
|
|
93
|
+
}
|
|
94
|
+
if (!Object.prototype.hasOwnProperty.call(event, 'timestamp')) {
|
|
95
|
+
Object.defineProperty(event, 'timestamp', { value: normalized.timestamp, enumerable: false, configurable: true });
|
|
96
|
+
}
|
|
97
|
+
return event;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function safeDeliver(listener, event) {
|
|
101
|
+
try {
|
|
102
|
+
listener(event);
|
|
103
|
+
} catch (err) {
|
|
104
|
+
console.warn('[event-bus] subscriber failed:', err?.message || err);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function decodeBrokerEvent(data) {
|
|
109
|
+
try {
|
|
110
|
+
const bytes = data instanceof Uint8Array ? data : new Uint8Array(data || []);
|
|
111
|
+
return JSON.parse(new TextDecoder().decode(bytes));
|
|
112
|
+
} catch {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function createNatsJetStreamBrokerClient(options = {}) {
|
|
118
|
+
const servers = options.url || process.env.KRADLE_EVENT_NATS_URL || process.env.NATS_URL;
|
|
119
|
+
const subject = options.subject || process.env.KRADLE_EVENT_NATS_SUBJECT || DEFAULT_EVENT_SUBJECT;
|
|
120
|
+
const stream = options.stream || process.env.KRADLE_EVENT_NATS_STREAM || DEFAULT_NATS_STREAM;
|
|
121
|
+
let connectionPromise;
|
|
122
|
+
let jetstreamPromise;
|
|
123
|
+
let managerPromise;
|
|
124
|
+
let state = { transport: 'nats-jetstream', status: 'connecting', durable: true, subject, stream };
|
|
125
|
+
|
|
126
|
+
async function connection() {
|
|
127
|
+
if (!connectionPromise) {
|
|
128
|
+
connectionPromise = import('@nats-io/transport-node')
|
|
129
|
+
.then(({ connect }) => connect({ servers }))
|
|
130
|
+
.catch((err) => {
|
|
131
|
+
state = { ...state, status: 'error', reason: redactSecretText(err?.message || String(err)) };
|
|
132
|
+
throw err;
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
return connectionPromise;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function manager() {
|
|
139
|
+
if (!managerPromise) {
|
|
140
|
+
managerPromise = Promise.all([connection(), import('@nats-io/jetstream')]).then(async ([nc, { jetstreamManager }]) => {
|
|
141
|
+
const jsm = await jetstreamManager(nc);
|
|
142
|
+
try {
|
|
143
|
+
await jsm.streams.info(stream);
|
|
144
|
+
} catch {
|
|
145
|
+
await jsm.streams.add({ name: stream, subjects: [subject], max_msgs: MAX_EVENTS });
|
|
146
|
+
}
|
|
147
|
+
state = { ...state, status: 'ok', reason: undefined };
|
|
148
|
+
return jsm;
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
return managerPromise;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async function jetstreamClient() {
|
|
155
|
+
if (!jetstreamPromise) {
|
|
156
|
+
jetstreamPromise = Promise.all([connection(), manager(), import('@nats-io/jetstream')]).then(([nc, _jsm, { jetstream }]) => jetstream(nc));
|
|
157
|
+
}
|
|
158
|
+
return jetstreamPromise;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
void manager().catch(() => {});
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
async publish(eventSubject, event) {
|
|
165
|
+
const js = await jetstreamClient();
|
|
166
|
+
const payload = new TextEncoder().encode(JSON.stringify(event));
|
|
167
|
+
return js.publish(eventSubject || subject, payload, { msgID: event.id }).catch((err) => {
|
|
168
|
+
state = { ...state, status: 'error', reason: redactSecretText(err?.message || String(err)) };
|
|
169
|
+
throw err;
|
|
170
|
+
});
|
|
171
|
+
},
|
|
172
|
+
|
|
173
|
+
subscribe(eventSubject, listener) {
|
|
174
|
+
let subscription;
|
|
175
|
+
let active = true;
|
|
176
|
+
connection().then((nc) => {
|
|
177
|
+
if (!active) return;
|
|
178
|
+
subscription = nc.subscribe(eventSubject || subject);
|
|
179
|
+
(async () => {
|
|
180
|
+
for await (const message of subscription) {
|
|
181
|
+
const event = decodeBrokerEvent(message.data);
|
|
182
|
+
if (event) listener(event);
|
|
183
|
+
}
|
|
184
|
+
})().catch((err) => console.warn('[event-bus] NATS subscription failed:', err?.message || err));
|
|
185
|
+
}).catch((err) => console.warn('[event-bus] NATS connection failed:', err?.message || err));
|
|
186
|
+
return () => {
|
|
187
|
+
active = false;
|
|
188
|
+
try { subscription?.unsubscribe(); } catch {}
|
|
189
|
+
};
|
|
190
|
+
},
|
|
191
|
+
|
|
192
|
+
async replaySince(eventSubject, cursor, limit = 100) {
|
|
193
|
+
const jsm = await manager();
|
|
194
|
+
const info = await jsm.streams.info(stream);
|
|
195
|
+
const firstSeq = Math.max(info.state.first_seq || 1, (info.state.last_seq || 0) - MAX_EVENTS + 1);
|
|
196
|
+
const lastSeq = info.state.last_seq || 0;
|
|
197
|
+
const events = [];
|
|
198
|
+
for (let seq = firstSeq; seq <= lastSeq && events.length < MAX_EVENTS; seq += 1) {
|
|
199
|
+
try {
|
|
200
|
+
const message = await jsm.streams.getMessage(stream, { seq });
|
|
201
|
+
if (message?.subject !== (eventSubject || subject)) continue;
|
|
202
|
+
const event = decodeBrokerEvent(message.data);
|
|
203
|
+
if (event) events.push(event);
|
|
204
|
+
} catch {}
|
|
205
|
+
}
|
|
206
|
+
if (!cursor) return events.slice(-limit);
|
|
207
|
+
const index = events.findIndex((event) => event.id === cursor);
|
|
208
|
+
return index < 0 ? [] : events.slice(index + 1, index + 1 + limit);
|
|
209
|
+
},
|
|
210
|
+
|
|
211
|
+
status() {
|
|
212
|
+
return sanitizeBrokerStatus(state);
|
|
213
|
+
},
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export function createMemoryEventTransport(options = {}) {
|
|
218
|
+
const listeners = new Set();
|
|
219
|
+
const events = Array.isArray(options.initialEvents) ? [...options.initialEvents] : loadPersistedEvents(MAX_EVENTS);
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
name: 'memory',
|
|
223
|
+
available: true,
|
|
224
|
+
required: false,
|
|
225
|
+
|
|
226
|
+
publish(event) {
|
|
227
|
+
const normalized = normalizeEvent(event);
|
|
228
|
+
const delivered = annotateEvent(event, normalized);
|
|
229
|
+
events.push(normalized);
|
|
230
|
+
if (events.length > MAX_EVENTS) events.splice(0, events.length - MAX_EVENTS);
|
|
231
|
+
persistEvent(normalized);
|
|
232
|
+
for (const listener of [...listeners]) safeDeliver(listener, delivered);
|
|
233
|
+
return normalized;
|
|
234
|
+
},
|
|
235
|
+
|
|
236
|
+
subscribe(listener) {
|
|
237
|
+
listeners.add(listener);
|
|
238
|
+
return () => listeners.delete(listener);
|
|
239
|
+
},
|
|
240
|
+
|
|
241
|
+
replaySince(cursor, limit = 100) {
|
|
242
|
+
if (!cursor) return events.slice(-limit);
|
|
243
|
+
const index = events.findIndex((event) => event.id === cursor);
|
|
244
|
+
if (index < 0) return [];
|
|
245
|
+
return events.slice(index + 1, index + 1 + limit);
|
|
246
|
+
},
|
|
247
|
+
|
|
248
|
+
status() {
|
|
249
|
+
return { transport: 'memory', status: 'ok', durable: false };
|
|
250
|
+
},
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export function createNatsJetStreamEventTransport(options = {}) {
|
|
255
|
+
const listeners = new Set();
|
|
256
|
+
const deliveredIds = new Map();
|
|
257
|
+
const subject = options.subject || process.env.KRADLE_EVENT_NATS_SUBJECT || DEFAULT_EVENT_SUBJECT;
|
|
258
|
+
const natsUrl = options.url || process.env.KRADLE_EVENT_NATS_URL || process.env.NATS_URL || '';
|
|
259
|
+
const required = options.required ?? process.env.KRADLE_EVENT_REQUIRE_BROKER === 'true';
|
|
260
|
+
const brokerClient = options.brokerClient || (natsUrl ? createNatsJetStreamBrokerClient({ url: natsUrl, subject }) : null);
|
|
261
|
+
let unavailableReason = natsUrl || brokerClient ? null : 'missing-nats-url';
|
|
262
|
+
|
|
263
|
+
function publishToBroker(event) {
|
|
264
|
+
const brokerStatus = sanitizeBrokerStatus(brokerClient?.status?.());
|
|
265
|
+
if (required && brokerStatus && brokerStatus.status !== 'ok') {
|
|
266
|
+
throw new Error(brokerStatus.reason || `broker-${brokerStatus.status}`);
|
|
267
|
+
}
|
|
268
|
+
if (brokerClient?.publish) return brokerClient.publish(subject, event);
|
|
269
|
+
unavailableReason = unavailableReason || 'nats-client-not-initialized';
|
|
270
|
+
if (required) throw new Error(unavailableReason);
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function deliverOnce(listener, event) {
|
|
275
|
+
const id = event?.id;
|
|
276
|
+
if (id) {
|
|
277
|
+
let ids = deliveredIds.get(listener);
|
|
278
|
+
if (!ids) {
|
|
279
|
+
ids = new Set();
|
|
280
|
+
deliveredIds.set(listener, ids);
|
|
281
|
+
}
|
|
282
|
+
if (ids.has(id)) return;
|
|
283
|
+
ids.add(id);
|
|
284
|
+
if (ids.size > MAX_EVENTS) ids.delete(ids.values().next().value);
|
|
285
|
+
}
|
|
286
|
+
safeDeliver(listener, event);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function deliverToLocalListeners(event) {
|
|
290
|
+
for (const listener of [...listeners]) deliverOnce(listener, event);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return {
|
|
294
|
+
name: 'nats-jetstream',
|
|
295
|
+
get available() {
|
|
296
|
+
return Boolean(brokerClient?.publish || natsUrl);
|
|
297
|
+
},
|
|
298
|
+
required,
|
|
299
|
+
|
|
300
|
+
publish(event) {
|
|
301
|
+
const normalized = normalizeEvent(event);
|
|
302
|
+
const delivered = annotateEvent(event, normalized);
|
|
303
|
+
const published = publishToBroker(normalized);
|
|
304
|
+
if (required) {
|
|
305
|
+
return Promise.resolve(published).then(() => {
|
|
306
|
+
unavailableReason = null;
|
|
307
|
+
persistEvent(normalized);
|
|
308
|
+
deliverToLocalListeners(delivered);
|
|
309
|
+
return normalized;
|
|
310
|
+
}, (err) => {
|
|
311
|
+
unavailableReason = redactSecretText(err?.message || String(err));
|
|
312
|
+
throw err;
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
persistEvent(normalized);
|
|
316
|
+
if (published?.catch) {
|
|
317
|
+
published.then(() => { unavailableReason = null; }, (err) => { unavailableReason = redactSecretText(err?.message || String(err)); });
|
|
318
|
+
}
|
|
319
|
+
deliverToLocalListeners(delivered);
|
|
320
|
+
return normalized;
|
|
321
|
+
},
|
|
322
|
+
|
|
323
|
+
subscribe(listener) {
|
|
324
|
+
listeners.add(listener);
|
|
325
|
+
const unsubscribe = brokerClient?.subscribe?.(subject, (event) => deliverOnce(listener, normalizeEvent(event)));
|
|
326
|
+
return () => {
|
|
327
|
+
listeners.delete(listener);
|
|
328
|
+
deliveredIds.delete(listener);
|
|
329
|
+
if (typeof unsubscribe === 'function') unsubscribe();
|
|
330
|
+
};
|
|
331
|
+
},
|
|
332
|
+
|
|
333
|
+
replaySince(cursor, limit = 100) {
|
|
334
|
+
if (brokerClient?.replaySince) return brokerClient.replaySince(subject, cursor, limit);
|
|
335
|
+
const persisted = loadPersistedEvents(MAX_EVENTS);
|
|
336
|
+
if (!cursor) return persisted.slice(-limit);
|
|
337
|
+
const index = persisted.findIndex((event) => event.id === cursor);
|
|
338
|
+
return index < 0 ? [] : persisted.slice(index + 1, index + 1 + limit);
|
|
339
|
+
},
|
|
340
|
+
|
|
341
|
+
status() {
|
|
342
|
+
if (unavailableReason && required) return { transport: 'nats-jetstream', status: 'error', reason: unavailableReason, durable: true };
|
|
343
|
+
if (brokerClient?.status) return sanitizeBrokerStatus(brokerClient.status());
|
|
344
|
+
return { transport: 'nats-jetstream', status: unavailableReason ? 'degraded' : 'ok', reason: unavailableReason, durable: true, subject };
|
|
345
|
+
},
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
export function createConfiguredEventTransport(env = process.env) {
|
|
350
|
+
const wantsNats = env.KRADLE_EVENT_TRANSPORT === 'nats' || env.KRADLE_EVENT_TRANSPORT === 'nats-jetstream' || env.KRADLE_EVENT_NATS_URL || env.NATS_URL;
|
|
351
|
+
return wantsNats
|
|
352
|
+
? createNatsJetStreamEventTransport({ url: env.KRADLE_EVENT_NATS_URL || env.NATS_URL, required: env.KRADLE_EVENT_REQUIRE_BROKER === 'true' })
|
|
353
|
+
: createMemoryEventTransport();
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
export function createEventBus(options = {}) {
|
|
357
|
+
const transport = options.transport || createConfiguredEventTransport(options.env || process.env);
|
|
358
|
+
const listenerUnsubscribes = new Map();
|
|
359
|
+
|
|
360
|
+
return {
|
|
361
|
+
subscribe(fn) {
|
|
362
|
+
const unsubscribe = transport.subscribe(fn);
|
|
363
|
+
listenerUnsubscribes.set(fn, unsubscribe);
|
|
364
|
+
return unsubscribe;
|
|
365
|
+
},
|
|
366
|
+
|
|
367
|
+
unsubscribe(fn) {
|
|
368
|
+
const unsubscribe = listenerUnsubscribes.get(fn);
|
|
369
|
+
if (unsubscribe) unsubscribe();
|
|
370
|
+
listenerUnsubscribes.delete(fn);
|
|
371
|
+
},
|
|
372
|
+
|
|
373
|
+
emit(event) {
|
|
374
|
+
return transport.publish(event);
|
|
375
|
+
},
|
|
376
|
+
|
|
377
|
+
emitResourceChange(kind, name, operation) {
|
|
378
|
+
return this.emit({
|
|
379
|
+
type: 'resource-change',
|
|
380
|
+
kind,
|
|
381
|
+
name,
|
|
382
|
+
operation,
|
|
383
|
+
timestamp: new Date().toISOString(),
|
|
384
|
+
});
|
|
385
|
+
},
|
|
386
|
+
|
|
387
|
+
replaySince(cursor, limit) {
|
|
388
|
+
return transport.replaySince(cursor, limit);
|
|
389
|
+
},
|
|
390
|
+
|
|
391
|
+
status() {
|
|
392
|
+
return transport.status();
|
|
393
|
+
},
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
export const globalEventBus = createEventBus();
|