@desplega.ai/agent-swarm 1.157.0 → 1.158.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{acp-adapter-qhqrk4q0.js → acp-adapter-xaq4n6pc.js} +1 -1
- package/dist/{actions-9rrg0zxm.js → actions-p9hz5yat.js} +15 -15
- package/dist/{app-enysy1zd.js → app-g26wdcz6.js} +4 -4
- package/dist/{artifact-td4q4dch.js → artifact-3v5t9jtg.js} +1 -1
- package/dist/{assistant-zegamy48.js → assistant-wydfqkb3.js} +16 -16
- package/dist/{boot-reembed-r6shx5qx.js → boot-reembed-g5ckg9yb.js} +6 -6
- package/dist/{boot-reembed-zfxb937k.js → boot-reembed-se8zmdcp.js} +5 -5
- package/dist/{boot-scrub-logs-k5y0se6r.js → boot-scrub-logs-wfxqan61.js} +3 -3
- package/dist/{seed-pricing-vqxa06qf.js → boot-seeds-s3q01xj3.js} +10 -10
- package/dist/{claude-adapter-5yc7hxwv.js → claude-adapter-tv2x4f36.js} +7 -7
- package/dist/{claude-managed-adapter-ea2aga6w.js → claude-managed-adapter-41b2s7fd.js} +2 -2
- package/dist/{claude-sdk-session-j4ywn3xf.js → claude-sdk-session-t442qz6f.js} +8 -8
- package/dist/{cli-e781c6f9.js → cli-0xjkj1j7.js} +1 -0
- package/dist/{cli-58rgvkt4.js → cli-1hkrch3k.js} +2 -2
- package/dist/{cli-yscm6cv8.js → cli-20cj37vt.js} +1866 -9
- package/dist/{cli-h0wdadsx.js → cli-4198qc9v.js} +7 -3
- package/dist/{cli-awwphhcs.js → cli-47m6dkd3.js} +362 -126
- package/dist/{cli-wb2zvbfg.js → cli-53sz8gkz.js} +1 -1
- package/dist/{cli-005pze6s.js → cli-5d10vd2m.js} +1 -1
- package/dist/{cli-v53rbqp3.js → cli-65hjmev5.js} +57 -17
- package/dist/{cli-scwq51gn.js → cli-68w09sjw.js} +7 -1
- package/dist/{cli-s98skd5r.js → cli-7gtvvpbh.js} +4 -4
- package/dist/{cli-y3nxwx0t.js → cli-7s3syk22.js} +18 -11
- package/dist/{cli-74h6f2bp.js → cli-8qmme6vd.js} +2 -2
- package/dist/{cli-wsed24mk.js → cli-987hg0aa.js} +2 -2
- package/dist/{cli-9n76jszt.js → cli-9httdpyq.js} +1 -1
- package/dist/{cli-f3va018d.js → cli-aky8tctm.js} +26 -93
- package/dist/{cli-sgyqyyxe.js → cli-c07r9a2m.js} +2 -1
- package/dist/{cli-c41wjx0r.js → cli-fgqxwrpv.js} +291 -12
- package/dist/{cli-01vfmq8c.js → cli-ftfvggzx.js} +607 -164
- package/dist/{cli-ytxs7mn1.js → cli-g8djp6qe.js} +17 -0
- package/dist/{cli-avkd6mqv.js → cli-h967nb6r.js} +551 -0
- package/dist/{cli-r9ee9c4w.js → cli-j55n64b6.js} +23 -22
- package/dist/{cli-9p33cz9h.js → cli-j57qkrpq.js} +86 -28
- package/dist/{cli-tjmghmm5.js → cli-q67y4wzm.js} +6 -3
- package/dist/{cli-n6zq3kbd.js → cli-rj19hc34.js} +1 -1
- package/dist/{cli-jytrw2na.js → cli-rjehvrts.js} +4 -4
- package/dist/{cli-tpv97m71.js → cli-rqz8qbw7.js} +245 -75
- package/dist/{cli-p37sg0a9.js → cli-s8eet254.js} +1 -1
- package/dist/cli-t1ze64h3.js +811 -0
- package/dist/{cli-6jjzxv3b.js → cli-ws6rqycm.js} +3 -2
- package/dist/{cli-xf5b7y1f.js → cli-wscdvcqz.js} +13 -5
- package/dist/{cli-5j7bm4tb.js → cli-x24pshtq.js} +5 -5
- package/dist/{cli-nbp6m6gp.js → cli-xberxa1r.js} +51 -26
- package/dist/{cli-vx9p3qdt.js → cli-xhaeg2nc.js} +1 -1
- package/dist/cli.js +29 -15
- package/dist/{codex-adapter-gns3s6c3.js → codex-adapter-cnm8wk11.js} +5 -5
- package/dist/{codex-session-runner-gqnxys5w.js → codex-session-runner-gfg547zj.js} +5 -5
- package/dist/{commands-hxy2y2sp.js → commands-e3bn0p75.js} +3 -3
- package/dist/{db-w8ghhsgf.js → db-03v9kzr2.js} +17 -3
- package/dist/{dispatcher-cp1rknjz.js → dispatcher-nrmg8yht.js} +14 -14
- package/dist/{dsh-adapter-1dwg97zv.js → dsh-adapter-k03nfqa6.js} +2 -2
- package/dist/{handlers-egfhr351.js → handlers-aqcedbyb.js} +17 -17
- package/dist/{hook-x4kftwzt.js → hook-7vkskkre.js} +19 -11
- package/dist/{hook-fbxjqfjz.js → hook-ybvmrmhq.js} +4 -4
- package/dist/{http-c7jfx990.js → http-2eqa8t24.js} +432 -195
- package/dist/{index-4k9hct21.js → index-1yks3qcr.js} +14 -14
- package/dist/{index-jtvpgb8f.js → index-a3vn96n8.js} +30 -27
- package/dist/{index-dc2ek6bw.js → index-c6zsv1bd.js} +5 -5
- package/dist/{index-yy07ef2s.js → index-chgdkq36.js} +14 -14
- package/dist/{index-r6bty6tb.js → index-x71bvq0p.js} +15 -15
- package/dist/{keepalive-f7ekefyc.js → keepalive-vtkf5n99.js} +5 -5
- package/dist/{lead-h053syts.js → lead-h3sp357x.js} +26 -26
- package/dist/{maintenance-a3w6zs54.js → maintenance-jzeqtk3q.js} +6 -6
- package/dist/{oauth-refresh-sweep-vra1dhkd.js → oauth-refresh-sweep-qp3dzmdp.js} +4 -4
- package/dist/{onboard-mfgzeqxs.js → onboard-tmsp0qbz.js} +278 -81
- package/dist/{opencode-adapter-z64xjzec.js → opencode-adapter-x6x2da1z.js} +3 -3
- package/dist/{otel-impl-2ez6xk57.js → otel-impl-9mz73q65.js} +416 -301
- package/dist/{pi-mono-adapter-2b7mw16r.js → pi-mono-adapter-m6dvv0dz.js} +2 -2
- package/dist/{pricing-refresh-6sw65tck.js → pricing-refresh-v1cbr9te.js} +5 -5
- package/dist/{rbac-roles-nm6924sy.js → rbac-roles-c9wqz83x.js} +4 -4
- package/dist/{render-v2-f026j6c4.js → render-v2-gryrh9h8.js} +4 -4
- package/dist/{setup-w606fr1q.js → setup-q9720bt6.js} +2 -2
- package/dist/{virtual-modules-8qheft18.js → virtual-modules-3htpgn8x.js} +1 -1
- package/dist/{work-objects-ezmggdne.js → work-objects-b76x7deb.js} +17 -17
- package/dist/{worker-1mbyv29k.js → worker-6vrxbv41.js} +26 -26
- package/openapi.json +1626 -629
- package/package.json +2 -1
- package/src/apps/catalog.generated.json +229 -0
- package/src/apps/page-validator.ts +71 -16
- package/src/apps/row-store.ts +66 -11
- package/src/apps/sync.ts +59 -17
- package/src/artifact-sdk/browser-sdk.ts +436 -2
- package/src/be/approval-sweeps.ts +109 -0
- package/src/be/boot-seeds.ts +40 -0
- package/src/be/chunking.ts +1 -1
- package/src/be/db/agents.ts +20 -0
- package/src/be/db/approvals.ts +78 -0
- package/src/be/db/tasks/read.ts +197 -39
- package/src/be/db/tasks/write.ts +41 -9
- package/src/be/db-client.ts +107 -48
- package/src/be/db.ts +219 -76
- package/src/be/events.ts +33 -1
- package/src/be/memory/graph-expansion.ts +10 -3
- package/src/be/memory/providers/sqlite-store.ts +55 -13
- package/src/be/memory/raters/retrieval.ts +5 -2
- package/src/be/memory/reranker.ts +26 -2
- package/src/be/memory/types.ts +2 -0
- package/src/be/migrations/166_approval_request_auto_cancellation.sql +9 -0
- package/src/be/migrations/171_memory_retrieval_relevance.sql +8 -0
- package/src/be/migrations/175_workflow_runs_list_index.sql +11 -0
- package/src/be/migrations/177_events_event_agent_created_index.sql +11 -0
- package/src/be/models-catalog.ts +2 -0
- package/src/be/modelsdev-cache.json +1857 -0
- package/src/be/modelsdev-cache.ts +14 -1
- package/src/be/pricing-normalize.ts +2 -0
- package/src/be/scripts/typecheck.ts +1 -0
- package/src/be/seed-pricing.ts +3 -1
- package/src/be/seed-skills/bundled-files.generated.json +1 -1
- package/src/be/steering.ts +53 -3
- package/src/be/swarm-config-guard.ts +3 -0
- package/src/be/task-citations.ts +29 -0
- package/src/cli.tsx +14 -1
- package/src/commands/onboard/compose-generator.ts +8 -3
- package/src/commands/onboard/container-engine.ts +220 -0
- package/src/commands/onboard/manifest.ts +1 -0
- package/src/commands/onboard/steps/health-check.tsx +4 -1
- package/src/commands/onboard/steps/prereq-check.tsx +50 -54
- package/src/commands/onboard/steps/start.tsx +31 -16
- package/src/commands/onboard/types.ts +7 -2
- package/src/commands/onboard.tsx +39 -4
- package/src/commands/profile-sync.ts +79 -26
- package/src/commands/runner.ts +8 -1
- package/src/extensions/catalog.generated.json +17 -0
- package/src/extensions/contract-types.generated.ts +1 -1
- package/src/heartbeat/heartbeat.ts +263 -103
- package/src/hooks/hook.ts +21 -12
- package/src/http/agents.ts +35 -0
- package/src/http/approval-requests.ts +136 -64
- package/src/http/apps.ts +14 -3
- package/src/http/config.ts +15 -1
- package/src/http/events.ts +62 -2
- package/src/http/index.ts +6 -4
- package/src/http/memory.ts +21 -7
- package/src/http/schedules.ts +1 -1
- package/src/http/session-data.ts +9 -4
- package/src/http/sessions.ts +25 -18
- package/src/http/stats.ts +19 -7
- package/src/http/status.ts +11 -1
- package/src/http/tasks.ts +40 -8
- package/src/http/webhooks.ts +67 -46
- package/src/http/workflows.ts +15 -20
- package/src/linear/outbound.ts +37 -15
- package/src/otel-impl.ts +151 -1
- package/src/otel.ts +14 -3
- package/src/prompts/memories.ts +19 -2
- package/src/providers/claude-managed-models.ts +8 -0
- package/src/providers/modelsdev-reasoning.json +551 -0
- package/src/rbac/legacy-policy.ts +16 -0
- package/src/rbac/permissions.ts +4 -0
- package/src/scheduler/schedule-task.ts +3 -0
- package/src/scripts-runtime/sdk-allowlist.ts +1 -0
- package/src/scripts-runtime/swarm-sdk.ts +9 -0
- package/src/scripts-runtime/types/stdlib.d.ts +1 -0
- package/src/scripts-runtime/types/swarm-extension.d.ts +1 -0
- package/src/scripts-runtime/types/swarm-sdk.d.ts +1 -0
- package/src/server.ts +12 -7
- package/src/slack/app.ts +35 -4
- package/src/slack/connection-state.ts +17 -0
- package/src/tasks/context-key.ts +1 -0
- package/src/tasks/worker-follow-up.ts +6 -2
- package/src/tests/agent-task-activity.test.ts +74 -0
- package/src/tests/agents-list-model-display.test.ts +6 -1
- package/src/tests/approval-auto-cancellation-migration.test.ts +104 -0
- package/src/tests/approval-requests.test.ts +395 -0
- package/src/tests/approval-sweeps-config.test.ts +47 -0
- package/src/tests/approval-sweeps.test.ts +508 -0
- package/src/tests/apps-kibo-blocks.test.ts +77 -0
- package/src/tests/apps-spike.test.ts +56 -1
- package/src/tests/apps-sync-engine.test.ts +20 -9
- package/src/tests/boot-seeds.test.ts +170 -0
- package/src/tests/cancel-approval-request-tool.test.ts +115 -0
- package/src/tests/claude-managed-adapter.test.ts +6 -0
- package/src/tests/config-key-filter.test.ts +80 -0
- package/src/tests/context-window.test.ts +1 -0
- package/src/tests/db-client-busy-retry.test.ts +96 -0
- package/src/tests/db-client.test.ts +60 -1
- package/src/tests/db-task-read-contract.test.ts +10 -0
- package/src/tests/db-task-write-contract.test.ts +9 -0
- package/src/tests/docs-index-footprint.test.js +3 -3
- package/src/tests/events-http.test.ts +103 -0
- package/src/tests/extensions-catalog.test.ts +22 -17
- package/src/tests/extensions-pre-task.test.ts +107 -0
- package/src/tests/fixtures/kibo-blocks-definition.json.txt +105 -0
- package/src/tests/follow-up-config-inheritance.test.ts +260 -0
- package/src/tests/heartbeat-orphan-resume.test.ts +84 -0
- package/src/tests/heartbeat-reboot-live.test.ts +95 -0
- package/src/tests/heartbeat-stall-cas.test.ts +80 -0
- package/src/tests/heartbeat.test.ts +99 -3
- package/src/tests/hook-mcp-config.test.ts +29 -0
- package/src/tests/http-api-integration.test.ts +16 -2
- package/src/tests/kv-storage.test.ts +62 -0
- package/src/tests/linear-outbound-sync.test.ts +120 -5
- package/src/tests/list-endpoint-slimming.test.ts +91 -0
- package/src/tests/memory-graph-organic-route.test.ts +155 -0
- package/src/tests/memory-http-recall-gating.test.ts +14 -4
- package/src/tests/memory-hybrid.test.ts +161 -1
- package/src/tests/memory-prompt-recall-crowdout.test.ts +297 -0
- package/src/tests/memory.test.ts +22 -0
- package/src/tests/onboard-container-engine.test.ts +240 -0
- package/src/tests/otel-event-loop-and-logs.test.ts +99 -0
- package/src/tests/pricing-routes.test.ts +14 -0
- package/src/tests/profile-sync-warning.test.ts +8 -4
- package/src/tests/promote-draft-task-route.test.ts +19 -0
- package/src/tests/provider-registration.test.ts +296 -42
- package/src/tests/rbac-engine.test.ts +30 -0
- package/src/tests/reasoning-effort.test.ts +8 -0
- package/src/tests/script-connections.test.ts +7 -0
- package/src/tests/sdk-allowlist.test.ts +39 -0
- package/src/tests/session-costs-model-key-normalize.test.ts +1 -0
- package/src/tests/sessions-detail-batch.test.ts +250 -0
- package/src/tests/sessions.test.ts +118 -0
- package/src/tests/slack-config.test.ts +22 -0
- package/src/tests/sonnet-5-5-registry.test.ts +44 -0
- package/src/tests/status.test.ts +36 -1
- package/src/tests/swarm-diff.test.ts +316 -10
- package/src/tests/task-completion-idempotency.test.ts +2 -1
- package/src/tests/tasks-requested-by-filter.test.ts +3 -1
- package/src/tests/tool-annotations.test.ts +1 -1
- package/src/tests/ui-list-slim-projections.test.ts +158 -0
- package/src/tests/webhooks-task-blocked.test.ts +127 -0
- package/src/tests/workflow-approval-recovery.test.ts +203 -0
- package/src/tests/workflow-hitl-routing.test.ts +22 -1
- package/src/tests/workflow-hitl-slack-card.test.ts +52 -0
- package/src/tests/workflow-http-v2.test.ts +99 -8
- package/src/tests/workflow-retry-validation.test.ts +56 -0
- package/src/tests/workflow-tla-races.test.ts +389 -0
- package/src/tests/workflow-validation-port-routing.test.ts +113 -1
- package/src/tools/cancel-approval-request.ts +57 -0
- package/src/tools/memory-search.ts +1 -0
- package/src/tools/request-human-input.ts +4 -2
- package/src/tools/send-task.ts +4 -0
- package/src/tools/templates.ts +19 -0
- package/src/tools/tool-config.ts +2 -1
- package/src/tools/workflows/list-workflow-runs.ts +1 -0
- package/src/types.ts +38 -0
- package/src/utils/context-window.ts +1 -0
- package/src/workflows/approval-cancel.ts +152 -0
- package/src/workflows/approval-notifications.ts +84 -0
- package/src/workflows/checkpoint.ts +10 -5
- package/src/workflows/definition.ts +21 -0
- package/src/workflows/engine.ts +152 -21
- package/src/workflows/executors/human-in-the-loop.ts +1 -1
- package/src/workflows/recovery.ts +100 -45
- package/src/workflows/resume.ts +49 -18
- package/src/workflows/retry-poller.ts +71 -21
- package/templates/extensions/README.md +1 -0
- package/templates/extensions/github-sender-allowlist/README.md +18 -0
- package/templates/extensions/github-sender-allowlist/hooks.ts +55 -0
- package/templates/extensions/github-sender-allowlist/manifest.json +8 -0
- package/templates/skills/apps/content.md +20 -1
- package/templates/skills/code-quality/content.md +26 -1
- package/templates/skills/pages/content.md +16 -4
- package/templates/skills/pages/files/examples/multiplayer-board.html +3 -12
- package/dist/cli-2daksqps.js +0 -378
- package/dist/rbac-roles-3gxmnb20.js +0 -37
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@desplega.ai/agent-swarm",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.158.0",
|
|
4
4
|
"description": "agent-swarm.dev — an open-source operating system for AI work: a lead agent breaks goals into tasks, routes them to specialized workers such as Claude Code or Codex, runs each worker in an isolated container, and preserves shared memory, tools, schedules, and review gates so delegated work compounds across sessions.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agent-swarm.dev",
|
|
@@ -203,6 +203,7 @@
|
|
|
203
203
|
"@opencode-ai/sdk": "^1.18.32",
|
|
204
204
|
"@openfort/openfort-node": "^0.9.1",
|
|
205
205
|
"@opentelemetry/api": "^1.9.1",
|
|
206
|
+
"@opentelemetry/api-logs": "^0.218.0",
|
|
206
207
|
"@opentelemetry/exporter-metrics-otlp-http": "^0.218.0",
|
|
207
208
|
"@opentelemetry/exporter-trace-otlp-http": "^0.218.0",
|
|
208
209
|
"@opentelemetry/resources": "^2.7.1",
|
|
@@ -21,6 +21,9 @@
|
|
|
21
21
|
"Form",
|
|
22
22
|
"Drawer",
|
|
23
23
|
"DetailList",
|
|
24
|
+
"Kanban",
|
|
25
|
+
"Calendar",
|
|
26
|
+
"ContributionGraph",
|
|
24
27
|
"ElementRef",
|
|
25
28
|
"ElementSlot"
|
|
26
29
|
],
|
|
@@ -842,6 +845,232 @@
|
|
|
842
845
|
"additionalProperties": false
|
|
843
846
|
}
|
|
844
847
|
},
|
|
848
|
+
"Kanban": {
|
|
849
|
+
"description": "Kanban board: one card per row, grouped into `columns` by the row's `columnField`. Bind `data`/`loading` to a named query. `titleField` is the card title; `cardFields` add label/value lines with Table column kinds (badge tones, relative dates). Dragging a card to another column runs `onMove` with `$row` = the moved row, `columnField` already set to the new column — typically `[{ \"action\": \"app.mutate\", \"params\": { \"model\": \"<m>\", \"op\": \"update\", \"rowId\": { \"$row\": \"id\" }, \"values\": { \"<columnField>\": { \"$row\": \"<columnField>\" } } } }]`. Without `onMove` the board is read-only. `onCardClick` runs with `$row` (e.g. an `app.navigate` that opens a Drawer).",
|
|
850
|
+
"props": {
|
|
851
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
852
|
+
"type": "object",
|
|
853
|
+
"properties": {
|
|
854
|
+
"data": {
|
|
855
|
+
"type": "array",
|
|
856
|
+
"items": {
|
|
857
|
+
"type": "object",
|
|
858
|
+
"propertyNames": {
|
|
859
|
+
"type": "string"
|
|
860
|
+
},
|
|
861
|
+
"additionalProperties": {}
|
|
862
|
+
}
|
|
863
|
+
},
|
|
864
|
+
"loading": {
|
|
865
|
+
"type": "boolean"
|
|
866
|
+
},
|
|
867
|
+
"columnField": {
|
|
868
|
+
"type": "string"
|
|
869
|
+
},
|
|
870
|
+
"columns": {
|
|
871
|
+
"minItems": 1,
|
|
872
|
+
"type": "array",
|
|
873
|
+
"items": {
|
|
874
|
+
"anyOf": [
|
|
875
|
+
{
|
|
876
|
+
"type": "string"
|
|
877
|
+
},
|
|
878
|
+
{
|
|
879
|
+
"type": "object",
|
|
880
|
+
"properties": {
|
|
881
|
+
"id": {
|
|
882
|
+
"type": "string"
|
|
883
|
+
},
|
|
884
|
+
"label": {
|
|
885
|
+
"type": "string"
|
|
886
|
+
}
|
|
887
|
+
},
|
|
888
|
+
"required": ["id"],
|
|
889
|
+
"additionalProperties": false
|
|
890
|
+
}
|
|
891
|
+
]
|
|
892
|
+
}
|
|
893
|
+
},
|
|
894
|
+
"titleField": {
|
|
895
|
+
"type": "string"
|
|
896
|
+
},
|
|
897
|
+
"cardFields": {
|
|
898
|
+
"type": "array",
|
|
899
|
+
"items": {
|
|
900
|
+
"type": "object",
|
|
901
|
+
"properties": {
|
|
902
|
+
"key": {
|
|
903
|
+
"type": "string"
|
|
904
|
+
},
|
|
905
|
+
"label": {
|
|
906
|
+
"type": "string"
|
|
907
|
+
},
|
|
908
|
+
"kind": {
|
|
909
|
+
"type": "string",
|
|
910
|
+
"enum": ["text", "string", "number", "boolean", "date", "badge", "enum", "code"]
|
|
911
|
+
},
|
|
912
|
+
"tones": {
|
|
913
|
+
"type": "object",
|
|
914
|
+
"propertyNames": {
|
|
915
|
+
"type": "string"
|
|
916
|
+
},
|
|
917
|
+
"additionalProperties": {
|
|
918
|
+
"type": "string",
|
|
919
|
+
"enum": [
|
|
920
|
+
"neutral",
|
|
921
|
+
"success",
|
|
922
|
+
"active",
|
|
923
|
+
"error",
|
|
924
|
+
"info",
|
|
925
|
+
"pending",
|
|
926
|
+
"warning",
|
|
927
|
+
"paused"
|
|
928
|
+
]
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
},
|
|
932
|
+
"required": ["key"],
|
|
933
|
+
"additionalProperties": false
|
|
934
|
+
}
|
|
935
|
+
},
|
|
936
|
+
"idField": {
|
|
937
|
+
"type": "string"
|
|
938
|
+
},
|
|
939
|
+
"onMove": {
|
|
940
|
+
"type": "array",
|
|
941
|
+
"items": {
|
|
942
|
+
"type": "object",
|
|
943
|
+
"properties": {
|
|
944
|
+
"action": {
|
|
945
|
+
"type": "string"
|
|
946
|
+
},
|
|
947
|
+
"params": {
|
|
948
|
+
"type": "object",
|
|
949
|
+
"propertyNames": {
|
|
950
|
+
"type": "string"
|
|
951
|
+
},
|
|
952
|
+
"additionalProperties": {}
|
|
953
|
+
}
|
|
954
|
+
},
|
|
955
|
+
"required": ["action"],
|
|
956
|
+
"additionalProperties": false
|
|
957
|
+
}
|
|
958
|
+
},
|
|
959
|
+
"onCardClick": {
|
|
960
|
+
"type": "array",
|
|
961
|
+
"items": {
|
|
962
|
+
"type": "object",
|
|
963
|
+
"properties": {
|
|
964
|
+
"action": {
|
|
965
|
+
"type": "string"
|
|
966
|
+
},
|
|
967
|
+
"params": {
|
|
968
|
+
"type": "object",
|
|
969
|
+
"propertyNames": {
|
|
970
|
+
"type": "string"
|
|
971
|
+
},
|
|
972
|
+
"additionalProperties": {}
|
|
973
|
+
}
|
|
974
|
+
},
|
|
975
|
+
"required": ["action"],
|
|
976
|
+
"additionalProperties": false
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
},
|
|
980
|
+
"required": ["columnField", "columns", "titleField"],
|
|
981
|
+
"additionalProperties": false
|
|
982
|
+
}
|
|
983
|
+
},
|
|
984
|
+
"Calendar": {
|
|
985
|
+
"description": "Read-only month calendar of rows. `startField` (and optional `endField`) hold dates; an event shows on every day it spans. `titleField` labels it. Has its own prev/next month controls; each Calendar keeps its own month. `onSelect` runs when an event is clicked, with `$row` (e.g. `app.navigate` to open a Drawer).",
|
|
986
|
+
"props": {
|
|
987
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
988
|
+
"type": "object",
|
|
989
|
+
"properties": {
|
|
990
|
+
"data": {
|
|
991
|
+
"type": "array",
|
|
992
|
+
"items": {
|
|
993
|
+
"type": "object",
|
|
994
|
+
"propertyNames": {
|
|
995
|
+
"type": "string"
|
|
996
|
+
},
|
|
997
|
+
"additionalProperties": {}
|
|
998
|
+
}
|
|
999
|
+
},
|
|
1000
|
+
"loading": {
|
|
1001
|
+
"type": "boolean"
|
|
1002
|
+
},
|
|
1003
|
+
"startField": {
|
|
1004
|
+
"type": "string"
|
|
1005
|
+
},
|
|
1006
|
+
"endField": {
|
|
1007
|
+
"type": "string"
|
|
1008
|
+
},
|
|
1009
|
+
"titleField": {
|
|
1010
|
+
"type": "string"
|
|
1011
|
+
},
|
|
1012
|
+
"idField": {
|
|
1013
|
+
"type": "string"
|
|
1014
|
+
},
|
|
1015
|
+
"onSelect": {
|
|
1016
|
+
"type": "array",
|
|
1017
|
+
"items": {
|
|
1018
|
+
"type": "object",
|
|
1019
|
+
"properties": {
|
|
1020
|
+
"action": {
|
|
1021
|
+
"type": "string"
|
|
1022
|
+
},
|
|
1023
|
+
"params": {
|
|
1024
|
+
"type": "object",
|
|
1025
|
+
"propertyNames": {
|
|
1026
|
+
"type": "string"
|
|
1027
|
+
},
|
|
1028
|
+
"additionalProperties": {}
|
|
1029
|
+
}
|
|
1030
|
+
},
|
|
1031
|
+
"required": ["action"],
|
|
1032
|
+
"additionalProperties": false
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
},
|
|
1036
|
+
"required": ["startField", "titleField"],
|
|
1037
|
+
"additionalProperties": false
|
|
1038
|
+
}
|
|
1039
|
+
},
|
|
1040
|
+
"ContributionGraph": {
|
|
1041
|
+
"description": "GitHub-style activity heatmap over the trailing `days` (default 365). Buckets rows by the calendar day of `dateField` (default `date`); each row counts 1, or sums `countField` when set. Read-only.",
|
|
1042
|
+
"props": {
|
|
1043
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
1044
|
+
"type": "object",
|
|
1045
|
+
"properties": {
|
|
1046
|
+
"data": {
|
|
1047
|
+
"type": "array",
|
|
1048
|
+
"items": {
|
|
1049
|
+
"type": "object",
|
|
1050
|
+
"propertyNames": {
|
|
1051
|
+
"type": "string"
|
|
1052
|
+
},
|
|
1053
|
+
"additionalProperties": {}
|
|
1054
|
+
}
|
|
1055
|
+
},
|
|
1056
|
+
"loading": {
|
|
1057
|
+
"type": "boolean"
|
|
1058
|
+
},
|
|
1059
|
+
"dateField": {
|
|
1060
|
+
"type": "string"
|
|
1061
|
+
},
|
|
1062
|
+
"countField": {
|
|
1063
|
+
"type": "string"
|
|
1064
|
+
},
|
|
1065
|
+
"days": {
|
|
1066
|
+
"type": "integer",
|
|
1067
|
+
"minimum": 7,
|
|
1068
|
+
"maximum": 730
|
|
1069
|
+
}
|
|
1070
|
+
},
|
|
1071
|
+
"additionalProperties": false
|
|
1072
|
+
}
|
|
1073
|
+
},
|
|
845
1074
|
"ElementRef": {
|
|
846
1075
|
"description": "Reference a reusable element from this app or an exported element from another app. Consumer children are inserted at the target's ElementSlot.",
|
|
847
1076
|
"slots": ["default"],
|
|
@@ -43,6 +43,24 @@ export const ELEMENT_KEYS = new Set([
|
|
|
43
43
|
"watch",
|
|
44
44
|
]);
|
|
45
45
|
const UI_STATE_COMPONENTS = new Set(["SearchInput", "Select", "Tabs"]);
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Props that hold a plain action chain (an array of `{ action, params }`),
|
|
49
|
+
* keyed by component type. Table's `rowActions[].actions` is nested one level
|
|
50
|
+
* deeper and handled separately.
|
|
51
|
+
*/
|
|
52
|
+
const PROP_ACTION_CHAINS: Record<string, readonly string[]> = {
|
|
53
|
+
Form: ["onSubmit"],
|
|
54
|
+
Kanban: ["onMove", "onCardClick"],
|
|
55
|
+
Calendar: ["onSelect"],
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/** Props naming a row field of the bound `data` query's model, per type. */
|
|
59
|
+
const DATA_FIELD_PROPS: Record<string, readonly string[]> = {
|
|
60
|
+
Kanban: ["columnField", "titleField", "idField"],
|
|
61
|
+
Calendar: ["startField", "endField", "titleField", "idField"],
|
|
62
|
+
ContributionGraph: ["dateField", "countField"],
|
|
63
|
+
};
|
|
46
64
|
const CONDITION_KEYS = new Set(["eq", "neq", "gt", "gte", "lt", "lte", "not"]);
|
|
47
65
|
|
|
48
66
|
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
@@ -705,8 +723,8 @@ function visitPageActionSteps(
|
|
|
705
723
|
);
|
|
706
724
|
}
|
|
707
725
|
}
|
|
708
|
-
|
|
709
|
-
visitActionChain(rawElement.props
|
|
726
|
+
for (const prop of PROP_ACTION_CHAINS[String(rawElement.type)] ?? []) {
|
|
727
|
+
visitActionChain(rawElement.props[prop], `${elementPath}.props.${prop}`, visit, false);
|
|
710
728
|
}
|
|
711
729
|
}
|
|
712
730
|
}
|
|
@@ -1234,7 +1252,7 @@ export function validatePage(
|
|
|
1234
1252
|
if (component) {
|
|
1235
1253
|
const propsPath = appendPath(elementPath, "props");
|
|
1236
1254
|
const actionChainPath = (path: string): boolean =>
|
|
1237
|
-
(type
|
|
1255
|
+
(PROP_ACTION_CHAINS[String(type)] ?? []).some((prop) => path === `${propsPath}.${prop}`) ||
|
|
1238
1256
|
(type === "Table" && /^.+\.rowActions\.\d+\.actions$/.test(path));
|
|
1239
1257
|
const propsResult = validateSchema(
|
|
1240
1258
|
Object.hasOwn(rawElement, "props") ? rawElement.props : {},
|
|
@@ -1331,6 +1349,44 @@ export function validatePage(
|
|
|
1331
1349
|
if (found) issues.push(found);
|
|
1332
1350
|
}
|
|
1333
1351
|
}
|
|
1352
|
+
} else if (typeof type === "string" && Object.hasOwn(DATA_FIELD_PROPS, type)) {
|
|
1353
|
+
// Kanban / Calendar / ContributionGraph name row fields of the bound
|
|
1354
|
+
// query's model (`columnField`, `startField`, ...) — a typo'd name
|
|
1355
|
+
// would silently render an empty board/grid, so cross-check it.
|
|
1356
|
+
const queryModel = queryModelFromDataBinding(definition, rawElement.props.data);
|
|
1357
|
+
if (queryModel) {
|
|
1358
|
+
const props = rawElement.props;
|
|
1359
|
+
const fieldNames: [string, unknown][] = (DATA_FIELD_PROPS[type] ?? []).map((prop) => [
|
|
1360
|
+
`${elementPath}.props.${prop}`,
|
|
1361
|
+
props[prop],
|
|
1362
|
+
]);
|
|
1363
|
+
if (type === "Kanban" && Array.isArray(props.cardFields)) {
|
|
1364
|
+
for (const [index, field] of props.cardFields.entries()) {
|
|
1365
|
+
if (isPlainObject(field)) {
|
|
1366
|
+
fieldNames.push([`${elementPath}.props.cardFields.${index}.key`, field.key]);
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
for (const [fieldPath, fieldName] of fieldNames) {
|
|
1371
|
+
if (typeof fieldName !== "string") continue;
|
|
1372
|
+
const found = fieldBindingIssue(
|
|
1373
|
+
queryModel.modelName,
|
|
1374
|
+
queryModel.model,
|
|
1375
|
+
fieldName,
|
|
1376
|
+
fieldPath,
|
|
1377
|
+
);
|
|
1378
|
+
if (found) issues.push(found);
|
|
1379
|
+
}
|
|
1380
|
+
for (const prop of PROP_ACTION_CHAINS[type] ?? []) {
|
|
1381
|
+
collectRowFieldIssues(
|
|
1382
|
+
props[prop],
|
|
1383
|
+
`${elementPath}.props.${prop}`,
|
|
1384
|
+
queryModel.modelName,
|
|
1385
|
+
queryModel.model,
|
|
1386
|
+
issues,
|
|
1387
|
+
);
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1334
1390
|
} else if (type === "Form") {
|
|
1335
1391
|
// `$`-prefixed names are reserved runtime slots under `/forms/<id>/`
|
|
1336
1392
|
// (`$error` carries the inline mutate failure) — a field stored there
|
|
@@ -1440,19 +1496,18 @@ export function validatePage(
|
|
|
1440
1496
|
stateRefs.push(...chainResult.stateRefs);
|
|
1441
1497
|
}
|
|
1442
1498
|
}
|
|
1443
|
-
if (
|
|
1444
|
-
type
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
stateRefs.push(...chainResult.stateRefs);
|
|
1499
|
+
if (isPlainObject(rawElement.props)) {
|
|
1500
|
+
for (const prop of PROP_ACTION_CHAINS[String(type)] ?? []) {
|
|
1501
|
+
if (!Object.hasOwn(rawElement.props, prop)) continue;
|
|
1502
|
+
const chainResult = validateActionChain(
|
|
1503
|
+
rawElement.props[prop],
|
|
1504
|
+
`${elementPath}.props.${prop}`,
|
|
1505
|
+
definition,
|
|
1506
|
+
catalog,
|
|
1507
|
+
);
|
|
1508
|
+
issues.push(...chainResult.issues);
|
|
1509
|
+
stateRefs.push(...chainResult.stateRefs);
|
|
1510
|
+
}
|
|
1456
1511
|
}
|
|
1457
1512
|
}
|
|
1458
1513
|
|
package/src/apps/row-store.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { deleteKv, getDbClient, getKv, listKv, upsertKv } from "../be/db";
|
|
1
|
+
import { deleteKv, getDbClient, getKv, kvPrefixUpperBound, listKv, upsertKv } from "../be/db";
|
|
2
|
+
import { isInTransaction } from "../be/db-client";
|
|
2
3
|
import {
|
|
3
4
|
AppDefinitionSchema,
|
|
4
5
|
type AppValidationIssue,
|
|
@@ -58,6 +59,15 @@ export class AppRowAppNotFoundError extends Error {
|
|
|
58
59
|
const mutationChains = new Map<string, Promise<unknown>>();
|
|
59
60
|
let lastCreatedAtMs = 0;
|
|
60
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Let queued I/O and other requests run. DB calls are synchronous, so a loop
|
|
64
|
+
* of awaited statements otherwise only drains microtasks and holds the event
|
|
65
|
+
* loop until it finishes.
|
|
66
|
+
*/
|
|
67
|
+
export function yieldToEventLoop(): Promise<void> {
|
|
68
|
+
return new Promise((resolve) => setImmediate(resolve));
|
|
69
|
+
}
|
|
70
|
+
|
|
61
71
|
export function appsNamespace(appId: string): string {
|
|
62
72
|
return `apps:${appId}`;
|
|
63
73
|
}
|
|
@@ -331,11 +341,23 @@ export async function getAppRow(
|
|
|
331
341
|
return entry.value as AppRow;
|
|
332
342
|
}
|
|
333
343
|
|
|
334
|
-
|
|
344
|
+
/**
|
|
345
|
+
* `equals` pre-filters rows in SQL on top-level fields so a filtered list
|
|
346
|
+
* does not decode the whole model. The match is looser than `===` (JSON
|
|
347
|
+
* `true` equals `1`); callers re-check the returned rows.
|
|
348
|
+
*/
|
|
349
|
+
export async function listAppRows(
|
|
350
|
+
appId: string,
|
|
351
|
+
model: string,
|
|
352
|
+
equals: Array<{ column: string; value: string | number | boolean }> = [],
|
|
353
|
+
): Promise<AppRow[]> {
|
|
335
354
|
const entries = await listKv(appsNamespace(appId), {
|
|
336
355
|
prefix: `${model}/row/`,
|
|
337
356
|
limit: 100000,
|
|
338
357
|
offset: 0,
|
|
358
|
+
jsonFieldEquals: equals
|
|
359
|
+
.filter((filter) => !/["\\]/.test(filter.column))
|
|
360
|
+
.map((filter) => ({ field: filter.column, value: filter.value })),
|
|
339
361
|
});
|
|
340
362
|
return entries
|
|
341
363
|
.map((entry) => entry.value)
|
|
@@ -355,17 +377,37 @@ export async function listAllAppRowsForMigrationUnlocked(
|
|
|
355
377
|
const rows: AppRow[] = [];
|
|
356
378
|
const namespace = appsNamespace(appId);
|
|
357
379
|
const prefix = `${model}/row/`;
|
|
358
|
-
|
|
380
|
+
// Row keys are `<model>/row/<uuid>`, so this bound always exists.
|
|
381
|
+
const upper = kvPrefixUpperBound(prefix)!;
|
|
382
|
+
// Keyset paging on the (namespace, key) primary key: OFFSET re-walked every
|
|
383
|
+
// skipped row on each page. Same rows, order and TTL filter as `listKv`.
|
|
384
|
+
let afterKey = "";
|
|
359
385
|
while (true) {
|
|
360
|
-
const
|
|
361
|
-
|
|
362
|
-
|
|
386
|
+
const page = await getDbClient().query<{ key: string; value: string; value_type: string }>(
|
|
387
|
+
`SELECT key, value, value_type FROM kv_entries
|
|
388
|
+
WHERE namespace = ? AND key >= ? AND key < ? AND key > ?
|
|
389
|
+
AND (expires_at IS NULL OR expires_at > ?)
|
|
390
|
+
ORDER BY key
|
|
391
|
+
LIMIT ?`,
|
|
392
|
+
[namespace, prefix, upper, afterKey, Date.now(), MIGRATION_KV_BATCH_SIZE],
|
|
393
|
+
);
|
|
394
|
+
for (const entry of page) {
|
|
395
|
+
if (entry.value_type !== "json") continue;
|
|
396
|
+
let value: unknown;
|
|
397
|
+
try {
|
|
398
|
+
value = JSON.parse(entry.value);
|
|
399
|
+
} catch {
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
363
402
|
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
364
403
|
rows.push(value as AppRow);
|
|
365
404
|
}
|
|
366
405
|
}
|
|
367
|
-
if (
|
|
368
|
-
|
|
406
|
+
if (page.length < MIGRATION_KV_BATCH_SIZE) return rows;
|
|
407
|
+
afterKey = page[page.length - 1]!.key;
|
|
408
|
+
// Schema migrations call this inside an open DbClient transaction, where a
|
|
409
|
+
// timer would stall every DB caller; only yield outside one.
|
|
410
|
+
if (!isInTransaction()) await yieldToEventLoop();
|
|
369
411
|
}
|
|
370
412
|
}
|
|
371
413
|
|
|
@@ -502,12 +544,25 @@ export function deleteAppRow(
|
|
|
502
544
|
});
|
|
503
545
|
}
|
|
504
546
|
|
|
547
|
+
const PURGE_BATCH_SIZE = 1000;
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* Batched DELETEs with an event-loop yield between them: one statement per
|
|
551
|
+
* row blocked every other request for the whole purge, and a single DELETE of
|
|
552
|
+
* the namespace would block for its full duration too. Expired entries go as
|
|
553
|
+
* well; a TTL-filtered listing would leave them orphaned.
|
|
554
|
+
*/
|
|
505
555
|
async function purgeNamespace(appId: string): Promise<void> {
|
|
506
556
|
const namespace = appsNamespace(appId);
|
|
507
557
|
while (true) {
|
|
508
|
-
const
|
|
509
|
-
|
|
510
|
-
|
|
558
|
+
const result = await getDbClient().run(
|
|
559
|
+
`DELETE FROM kv_entries
|
|
560
|
+
WHERE namespace = ?
|
|
561
|
+
AND key IN (SELECT key FROM kv_entries WHERE namespace = ? ORDER BY key LIMIT ?)`,
|
|
562
|
+
[namespace, namespace, PURGE_BATCH_SIZE],
|
|
563
|
+
);
|
|
564
|
+
if (result.changes === 0) return;
|
|
565
|
+
await yieldToEventLoop();
|
|
511
566
|
}
|
|
512
567
|
}
|
|
513
568
|
|
package/src/apps/sync.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as z from "zod";
|
|
2
|
-
import { deleteKv, getAllTasks, getKv, type TaskFilters, upsertKv } from "../be/db";
|
|
2
|
+
import { deleteKv, getAllTasks, getDbClient, getKv, type TaskFilters, upsertKv } from "../be/db";
|
|
3
3
|
import { listScriptConnections } from "../be/script-connections";
|
|
4
4
|
import { getScriptById } from "../be/scripts/db";
|
|
5
5
|
import { runSavedScriptAsAgent } from "../be/scripts/run-saved";
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
listAllAppRowsForMigrationUnlocked,
|
|
22
22
|
patchAppRowUnlocked,
|
|
23
23
|
withMutationLock,
|
|
24
|
+
yieldToEventLoop,
|
|
24
25
|
} from "./row-store";
|
|
25
26
|
import { appDefinitionNeedsRepair, getApp } from "./store";
|
|
26
27
|
import { resolveSyncRunAs } from "./sync-run-as";
|
|
@@ -585,6 +586,45 @@ type ReconcileCounts = {
|
|
|
585
586
|
staleSweepSkipped?: boolean;
|
|
586
587
|
};
|
|
587
588
|
|
|
589
|
+
type ReconcileTally = Omit<ReconcileCounts, "staleSweepSkipped">;
|
|
590
|
+
|
|
591
|
+
/** Rows written per transaction. Big enough to amortize COMMIT, small enough
|
|
592
|
+
* that one chunk holds the event loop for milliseconds, not seconds. */
|
|
593
|
+
export const RECONCILE_CHUNK_SIZE = 200;
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* Apply `write` to `items` in transactions of RECONCILE_CHUNK_SIZE, yielding
|
|
597
|
+
* to the event loop between chunks. A chunk's tally lands in `counts` only
|
|
598
|
+
* after it commits, so a failing chunk rolls back its writes and its counts
|
|
599
|
+
* together while earlier chunks stay committed and counted.
|
|
600
|
+
*/
|
|
601
|
+
async function writeInChunks<T>(
|
|
602
|
+
items: readonly T[],
|
|
603
|
+
counts: ReconcileCounts,
|
|
604
|
+
write: (item: T, tally: ReconcileTally) => Promise<void>,
|
|
605
|
+
): Promise<void> {
|
|
606
|
+
for (let start = 0; start < items.length; start += RECONCILE_CHUNK_SIZE) {
|
|
607
|
+
const chunk = items.slice(start, start + RECONCILE_CHUNK_SIZE);
|
|
608
|
+
const tally = await getDbClient().transaction(async () => {
|
|
609
|
+
const chunkTally: ReconcileTally = {
|
|
610
|
+
created: 0,
|
|
611
|
+
updated: 0,
|
|
612
|
+
refreshed: 0,
|
|
613
|
+
unchanged: 0,
|
|
614
|
+
markedStale: 0,
|
|
615
|
+
};
|
|
616
|
+
for (const item of chunk) await write(item, chunkTally);
|
|
617
|
+
return chunkTally;
|
|
618
|
+
});
|
|
619
|
+
counts.created += tally.created;
|
|
620
|
+
counts.updated += tally.updated;
|
|
621
|
+
counts.refreshed += tally.refreshed;
|
|
622
|
+
counts.unchanged += tally.unchanged;
|
|
623
|
+
counts.markedStale += tally.markedStale;
|
|
624
|
+
await yieldToEventLoop();
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
588
628
|
function reconcile(args: {
|
|
589
629
|
appId: string;
|
|
590
630
|
model: string;
|
|
@@ -594,13 +634,15 @@ function reconcile(args: {
|
|
|
594
634
|
pull: PullResult;
|
|
595
635
|
warnings: string[];
|
|
596
636
|
/**
|
|
597
|
-
* Mutated
|
|
598
|
-
* that actually committed
|
|
599
|
-
*
|
|
637
|
+
* Mutated chunk-by-chunk so a mid-pass failure still reports the row churn
|
|
638
|
+
* that actually committed: writes run in chunked transactions, and a thrown
|
|
639
|
+
* error must not zero out the chunks that landed before it.
|
|
600
640
|
*/
|
|
601
641
|
counts: ReconcileCounts;
|
|
602
642
|
}): Promise<void> {
|
|
603
643
|
const { appId, model, sourceName, joinKey, pull, warnings, counts } = args;
|
|
644
|
+
// Lock order: the mutation lock is taken here, and every chunk transaction
|
|
645
|
+
// opens beneath it (see withMutationLock).
|
|
604
646
|
return withMutationLock(appId, model, async () => {
|
|
605
647
|
// Re-read under the lock: the pull ran unlocked, so the definition it was
|
|
606
648
|
// planned against may be gone. Anything that moves the identity or the
|
|
@@ -639,7 +681,7 @@ function reconcile(args: {
|
|
|
639
681
|
const actor = `sync:${sourceName}`;
|
|
640
682
|
const seen = new Set<string>();
|
|
641
683
|
|
|
642
|
-
|
|
684
|
+
await writeInChunks(pull.records, counts, async (record, tally) => {
|
|
643
685
|
if (seen.has(record.key)) {
|
|
644
686
|
warn(warnings, `source returned duplicate key "${record.key}"; the last record wins`);
|
|
645
687
|
}
|
|
@@ -655,8 +697,8 @@ function reconcile(args: {
|
|
|
655
697
|
actor,
|
|
656
698
|
});
|
|
657
699
|
mine.set(record.key, created);
|
|
658
|
-
|
|
659
|
-
|
|
700
|
+
tally.created += 1;
|
|
701
|
+
return;
|
|
660
702
|
}
|
|
661
703
|
const differs = Object.entries(values).some(
|
|
662
704
|
([name, value]) => !sameValue(existing[name], value),
|
|
@@ -671,12 +713,12 @@ function reconcile(args: {
|
|
|
671
713
|
});
|
|
672
714
|
if (!updated) {
|
|
673
715
|
warn(warnings, `row "${existing.id}" vanished before it could be updated`);
|
|
674
|
-
|
|
716
|
+
return;
|
|
675
717
|
}
|
|
676
718
|
mine.set(record.key, updated);
|
|
677
|
-
if (differs)
|
|
678
|
-
else
|
|
679
|
-
}
|
|
719
|
+
if (differs) tally.updated += 1;
|
|
720
|
+
else tally.refreshed += 1;
|
|
721
|
+
});
|
|
680
722
|
|
|
681
723
|
const unseen = [...mine.entries()]
|
|
682
724
|
.filter(([key]) => !seen.has(key))
|
|
@@ -688,10 +730,10 @@ function reconcile(args: {
|
|
|
688
730
|
warn(warnings, "pull reported an incomplete window; stale sweep skipped");
|
|
689
731
|
return;
|
|
690
732
|
}
|
|
691
|
-
|
|
733
|
+
await writeInChunks(unseen, counts, async (row, tally) => {
|
|
692
734
|
if (row.stale === true) {
|
|
693
|
-
|
|
694
|
-
|
|
735
|
+
tally.unchanged += 1;
|
|
736
|
+
return;
|
|
695
737
|
}
|
|
696
738
|
await patchAppRowUnlocked(
|
|
697
739
|
appId,
|
|
@@ -710,8 +752,8 @@ function reconcile(args: {
|
|
|
710
752
|
},
|
|
711
753
|
},
|
|
712
754
|
);
|
|
713
|
-
|
|
714
|
-
}
|
|
755
|
+
tally.markedStale += 1;
|
|
756
|
+
});
|
|
715
757
|
});
|
|
716
758
|
}
|
|
717
759
|
|
|
@@ -778,7 +820,7 @@ async function executePass(args: {
|
|
|
778
820
|
return scrubbed;
|
|
779
821
|
};
|
|
780
822
|
|
|
781
|
-
// Reconcile mutates this accumulator
|
|
823
|
+
// Reconcile mutates this accumulator chunk-by-chunk, so the error path below
|
|
782
824
|
// reports the churn that actually committed instead of the zero-count base.
|
|
783
825
|
const counts: ReconcileCounts = {
|
|
784
826
|
created: 0,
|