@crowdsource.you/core 1.2.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/README.md +571 -0
- package/dist/cases.d.ts +66 -0
- package/dist/cases.d.ts.map +1 -0
- package/dist/cases.js +79 -0
- package/dist/cases.js.map +1 -0
- package/dist/client.d.ts +90 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +125 -0
- package/dist/client.js.map +1 -0
- package/dist/communityNotes.d.ts +49 -0
- package/dist/communityNotes.d.ts.map +1 -0
- package/dist/communityNotes.js +104 -0
- package/dist/communityNotes.js.map +1 -0
- package/dist/credential.d.ts +58 -0
- package/dist/credential.d.ts.map +1 -0
- package/dist/credential.js +71 -0
- package/dist/credential.js.map +1 -0
- package/dist/defaults.d.ts +61 -0
- package/dist/defaults.d.ts.map +1 -0
- package/dist/defaults.js +82 -0
- package/dist/defaults.js.map +1 -0
- package/dist/digest.d.ts +44 -0
- package/dist/digest.d.ts.map +1 -0
- package/dist/digest.js +79 -0
- package/dist/digest.js.map +1 -0
- package/dist/envelope.d.ts +143 -0
- package/dist/envelope.d.ts.map +1 -0
- package/dist/envelope.js +258 -0
- package/dist/envelope.js.map +1 -0
- package/dist/errors.d.ts +90 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +151 -0
- package/dist/errors.js.map +1 -0
- package/dist/esm/cases.js +74 -0
- package/dist/esm/cases.js.map +1 -0
- package/dist/esm/client.js +121 -0
- package/dist/esm/client.js.map +1 -0
- package/dist/esm/communityNotes.js +100 -0
- package/dist/esm/communityNotes.js.map +1 -0
- package/dist/esm/credential.js +67 -0
- package/dist/esm/credential.js.map +1 -0
- package/dist/esm/defaults.js +77 -0
- package/dist/esm/defaults.js.map +1 -0
- package/dist/esm/digest.js +74 -0
- package/dist/esm/digest.js.map +1 -0
- package/dist/esm/envelope.js +252 -0
- package/dist/esm/envelope.js.map +1 -0
- package/dist/esm/errors.js +140 -0
- package/dist/esm/errors.js.map +1 -0
- package/dist/esm/express/index.js +29 -0
- package/dist/esm/express/index.js.map +1 -0
- package/dist/esm/express/middleware.js +293 -0
- package/dist/esm/express/middleware.js.map +1 -0
- package/dist/esm/express/store.js +69 -0
- package/dist/esm/express/store.js.map +1 -0
- package/dist/esm/express/verify.js +133 -0
- package/dist/esm/express/verify.js.map +1 -0
- package/dist/esm/index.js +57 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/outbox/client.js +43 -0
- package/dist/esm/outbox/client.js.map +1 -0
- package/dist/esm/outbox/decision.js +153 -0
- package/dist/esm/outbox/decision.js.map +1 -0
- package/dist/esm/outbox/delivery.js +129 -0
- package/dist/esm/outbox/delivery.js.map +1 -0
- package/dist/esm/outbox/enforcement/executor.js +219 -0
- package/dist/esm/outbox/enforcement/executor.js.map +1 -0
- package/dist/esm/outbox/enforcement/planner.js +326 -0
- package/dist/esm/outbox/enforcement/planner.js.map +1 -0
- package/dist/esm/outbox/evidence.js +173 -0
- package/dist/esm/outbox/evidence.js.map +1 -0
- package/dist/esm/outbox/inbound.js +87 -0
- package/dist/esm/outbox/inbound.js.map +1 -0
- package/dist/esm/outbox/index.js +71 -0
- package/dist/esm/outbox/index.js.map +1 -0
- package/dist/esm/outbox/intake.js +140 -0
- package/dist/esm/outbox/intake.js.map +1 -0
- package/dist/esm/outbox/integration.js +116 -0
- package/dist/esm/outbox/integration.js.map +1 -0
- package/dist/esm/outbox/outbox/dispatcher.js +114 -0
- package/dist/esm/outbox/outbox/dispatcher.js.map +1 -0
- package/dist/esm/outbox/outbox/service.js +271 -0
- package/dist/esm/outbox/outbox/service.js.map +1 -0
- package/dist/esm/outbox/postgres/index.js +41 -0
- package/dist/esm/outbox/postgres/index.js.map +1 -0
- package/dist/esm/outbox/postgres/registries.js +155 -0
- package/dist/esm/outbox/postgres/registries.js.map +1 -0
- package/dist/esm/outbox/postgres/reportColumns.js +191 -0
- package/dist/esm/outbox/postgres/reportColumns.js.map +1 -0
- package/dist/esm/outbox/postgres/store/enforcement.js +158 -0
- package/dist/esm/outbox/postgres/store/enforcement.js.map +1 -0
- package/dist/esm/outbox/postgres/store/events.js +101 -0
- package/dist/esm/outbox/postgres/store/events.js.map +1 -0
- package/dist/esm/outbox/postgres/store/index.js +59 -0
- package/dist/esm/outbox/postgres/store/index.js.map +1 -0
- package/dist/esm/outbox/postgres/store/outbox.js +207 -0
- package/dist/esm/outbox/postgres/store/outbox.js.map +1 -0
- package/dist/esm/outbox/postgres/store/reports.js +231 -0
- package/dist/esm/outbox/postgres/store/reports.js.map +1 -0
- package/dist/esm/outbox/postgres/store/transaction.js +29 -0
- package/dist/esm/outbox/postgres/store/transaction.js.map +1 -0
- package/dist/esm/outbox/postgres/tables.js +270 -0
- package/dist/esm/outbox/postgres/tables.js.map +1 -0
- package/dist/esm/outbox/reconciliation.js +157 -0
- package/dist/esm/outbox/reconciliation.js.map +1 -0
- package/dist/esm/outbox/reportStatus.js +22 -0
- package/dist/esm/outbox/reportStatus.js.map +1 -0
- package/dist/esm/outbox/retention.js +33 -0
- package/dist/esm/outbox/retention.js.map +1 -0
- package/dist/esm/outbox/store/types.js +30 -0
- package/dist/esm/outbox/store/types.js.map +1 -0
- package/dist/esm/outbox/types.js +23 -0
- package/dist/esm/outbox/types.js.map +1 -0
- package/dist/esm/outbox/webhook.js +145 -0
- package/dist/esm/outbox/webhook.js.map +1 -0
- package/dist/esm/package.json +1 -0
- package/dist/esm/reports.js +74 -0
- package/dist/esm/reports.js.map +1 -0
- package/dist/esm/testing/fixtures.js +132 -0
- package/dist/esm/testing/fixtures.js.map +1 -0
- package/dist/esm/testing/index.js +33 -0
- package/dist/esm/testing/index.js.map +1 -0
- package/dist/esm/testing/sandbox.js +337 -0
- package/dist/esm/testing/sandbox.js.map +1 -0
- package/dist/esm/testing/webhook-simulator.js +93 -0
- package/dist/esm/testing/webhook-simulator.js.map +1 -0
- package/dist/esm/transport.js +177 -0
- package/dist/esm/transport.js.map +1 -0
- package/dist/esm/webhookEndpoints.js +139 -0
- package/dist/esm/webhookEndpoints.js.map +1 -0
- package/dist/express/index.d.ts +32 -0
- package/dist/express/index.d.ts.map +1 -0
- package/dist/express/index.js +40 -0
- package/dist/express/index.js.map +1 -0
- package/dist/express/middleware.d.ts +97 -0
- package/dist/express/middleware.d.ts.map +1 -0
- package/dist/express/middleware.js +298 -0
- package/dist/express/middleware.js.map +1 -0
- package/dist/express/store.d.ts +45 -0
- package/dist/express/store.d.ts.map +1 -0
- package/dist/express/store.js +72 -0
- package/dist/express/store.js.map +1 -0
- package/dist/express/verify.d.ts +86 -0
- package/dist/express/verify.d.ts.map +1 -0
- package/dist/express/verify.js +138 -0
- package/dist/express/verify.js.map +1 -0
- package/dist/index.d.ts +67 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +89 -0
- package/dist/index.js.map +1 -0
- package/dist/outbox/client.d.ts +35 -0
- package/dist/outbox/client.d.ts.map +1 -0
- package/dist/outbox/client.js +46 -0
- package/dist/outbox/client.js.map +1 -0
- package/dist/outbox/decision.d.ts +36 -0
- package/dist/outbox/decision.d.ts.map +1 -0
- package/dist/outbox/decision.js +159 -0
- package/dist/outbox/decision.js.map +1 -0
- package/dist/outbox/delivery.d.ts +50 -0
- package/dist/outbox/delivery.d.ts.map +1 -0
- package/dist/outbox/delivery.js +135 -0
- package/dist/outbox/delivery.js.map +1 -0
- package/dist/outbox/enforcement/executor.d.ts +26 -0
- package/dist/outbox/enforcement/executor.d.ts.map +1 -0
- package/dist/outbox/enforcement/executor.js +222 -0
- package/dist/outbox/enforcement/executor.js.map +1 -0
- package/dist/outbox/enforcement/planner.d.ts +50 -0
- package/dist/outbox/enforcement/planner.d.ts.map +1 -0
- package/dist/outbox/enforcement/planner.js +333 -0
- package/dist/outbox/enforcement/planner.js.map +1 -0
- package/dist/outbox/evidence.d.ts +114 -0
- package/dist/outbox/evidence.d.ts.map +1 -0
- package/dist/outbox/evidence.js +181 -0
- package/dist/outbox/evidence.js.map +1 -0
- package/dist/outbox/inbound.d.ts +78 -0
- package/dist/outbox/inbound.d.ts.map +1 -0
- package/dist/outbox/inbound.js +91 -0
- package/dist/outbox/inbound.js.map +1 -0
- package/dist/outbox/index.d.ts +79 -0
- package/dist/outbox/index.d.ts.map +1 -0
- package/dist/outbox/index.js +98 -0
- package/dist/outbox/index.js.map +1 -0
- package/dist/outbox/intake.d.ts +62 -0
- package/dist/outbox/intake.d.ts.map +1 -0
- package/dist/outbox/intake.js +145 -0
- package/dist/outbox/intake.js.map +1 -0
- package/dist/outbox/integration.d.ts +62 -0
- package/dist/outbox/integration.d.ts.map +1 -0
- package/dist/outbox/integration.js +119 -0
- package/dist/outbox/integration.js.map +1 -0
- package/dist/outbox/outbox/dispatcher.d.ts +26 -0
- package/dist/outbox/outbox/dispatcher.d.ts.map +1 -0
- package/dist/outbox/outbox/dispatcher.js +119 -0
- package/dist/outbox/outbox/dispatcher.js.map +1 -0
- package/dist/outbox/outbox/service.d.ts +124 -0
- package/dist/outbox/outbox/service.d.ts.map +1 -0
- package/dist/outbox/outbox/service.js +279 -0
- package/dist/outbox/outbox/service.js.map +1 -0
- package/dist/outbox/postgres/index.d.ts +44 -0
- package/dist/outbox/postgres/index.d.ts.map +1 -0
- package/dist/outbox/postgres/index.js +51 -0
- package/dist/outbox/postgres/index.js.map +1 -0
- package/dist/outbox/postgres/registries.d.ts +69 -0
- package/dist/outbox/postgres/registries.d.ts.map +1 -0
- package/dist/outbox/postgres/registries.js +159 -0
- package/dist/outbox/postgres/registries.js.map +1 -0
- package/dist/outbox/postgres/reportColumns.d.ts +154 -0
- package/dist/outbox/postgres/reportColumns.d.ts.map +1 -0
- package/dist/outbox/postgres/reportColumns.js +195 -0
- package/dist/outbox/postgres/reportColumns.js.map +1 -0
- package/dist/outbox/postgres/store/enforcement.d.ts +33 -0
- package/dist/outbox/postgres/store/enforcement.d.ts.map +1 -0
- package/dist/outbox/postgres/store/enforcement.js +161 -0
- package/dist/outbox/postgres/store/enforcement.js.map +1 -0
- package/dist/outbox/postgres/store/events.d.ts +46 -0
- package/dist/outbox/postgres/store/events.d.ts.map +1 -0
- package/dist/outbox/postgres/store/events.js +104 -0
- package/dist/outbox/postgres/store/events.js.map +1 -0
- package/dist/outbox/postgres/store/index.d.ts +27 -0
- package/dist/outbox/postgres/store/index.d.ts.map +1 -0
- package/dist/outbox/postgres/store/index.js +62 -0
- package/dist/outbox/postgres/store/index.js.map +1 -0
- package/dist/outbox/postgres/store/outbox.d.ts +8 -0
- package/dist/outbox/postgres/store/outbox.d.ts.map +1 -0
- package/dist/outbox/postgres/store/outbox.js +210 -0
- package/dist/outbox/postgres/store/outbox.js.map +1 -0
- package/dist/outbox/postgres/store/reports.d.ts +69 -0
- package/dist/outbox/postgres/store/reports.d.ts.map +1 -0
- package/dist/outbox/postgres/store/reports.js +234 -0
- package/dist/outbox/postgres/store/reports.js.map +1 -0
- package/dist/outbox/postgres/store/transaction.d.ts +49 -0
- package/dist/outbox/postgres/store/transaction.d.ts.map +1 -0
- package/dist/outbox/postgres/store/transaction.js +32 -0
- package/dist/outbox/postgres/store/transaction.js.map +1 -0
- package/dist/outbox/postgres/tables.d.ts +737 -0
- package/dist/outbox/postgres/tables.d.ts.map +1 -0
- package/dist/outbox/postgres/tables.js +273 -0
- package/dist/outbox/postgres/tables.js.map +1 -0
- package/dist/outbox/reconciliation.d.ts +49 -0
- package/dist/outbox/reconciliation.d.ts.map +1 -0
- package/dist/outbox/reconciliation.js +162 -0
- package/dist/outbox/reconciliation.js.map +1 -0
- package/dist/outbox/reportStatus.d.ts +12 -0
- package/dist/outbox/reportStatus.d.ts.map +1 -0
- package/dist/outbox/reportStatus.js +25 -0
- package/dist/outbox/reportStatus.js.map +1 -0
- package/dist/outbox/retention.d.ts +33 -0
- package/dist/outbox/retention.d.ts.map +1 -0
- package/dist/outbox/retention.js +36 -0
- package/dist/outbox/retention.js.map +1 -0
- package/dist/outbox/store/types.d.ts +466 -0
- package/dist/outbox/store/types.d.ts.map +1 -0
- package/dist/outbox/store/types.js +31 -0
- package/dist/outbox/store/types.js.map +1 -0
- package/dist/outbox/types.d.ts +660 -0
- package/dist/outbox/types.d.ts.map +1 -0
- package/dist/outbox/types.js +24 -0
- package/dist/outbox/types.js.map +1 -0
- package/dist/outbox/webhook.d.ts +15 -0
- package/dist/outbox/webhook.d.ts.map +1 -0
- package/dist/outbox/webhook.js +148 -0
- package/dist/outbox/webhook.js.map +1 -0
- package/dist/reports.d.ts +73 -0
- package/dist/reports.d.ts.map +1 -0
- package/dist/reports.js +78 -0
- package/dist/reports.js.map +1 -0
- package/dist/testing/fixtures.d.ts +62 -0
- package/dist/testing/fixtures.d.ts.map +1 -0
- package/dist/testing/fixtures.js +137 -0
- package/dist/testing/fixtures.js.map +1 -0
- package/dist/testing/index.d.ts +36 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +43 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/testing/sandbox.d.ts +109 -0
- package/dist/testing/sandbox.d.ts.map +1 -0
- package/dist/testing/sandbox.js +342 -0
- package/dist/testing/sandbox.js.map +1 -0
- package/dist/testing/webhook-simulator.d.ts +78 -0
- package/dist/testing/webhook-simulator.d.ts.map +1 -0
- package/dist/testing/webhook-simulator.js +98 -0
- package/dist/testing/webhook-simulator.js.map +1 -0
- package/dist/transport.d.ts +59 -0
- package/dist/transport.d.ts.map +1 -0
- package/dist/transport.js +181 -0
- package/dist/transport.js.map +1 -0
- package/dist/webhookEndpoints.d.ts +147 -0
- package/dist/webhookEndpoints.d.ts.map +1 -0
- package/dist/webhookEndpoints.js +143 -0
- package/dist/webhookEndpoints.js.map +1 -0
- package/package.json +127 -0
- package/src/cases.ts +119 -0
- package/src/client.ts +202 -0
- package/src/communityNotes.ts +194 -0
- package/src/credential.ts +100 -0
- package/src/defaults.ts +87 -0
- package/src/digest.ts +91 -0
- package/src/envelope.ts +432 -0
- package/src/errors.ts +165 -0
- package/src/express/index.ts +43 -0
- package/src/express/middleware.ts +387 -0
- package/src/express/store.ts +85 -0
- package/src/express/verify.ts +166 -0
- package/src/index.ts +107 -0
- package/src/outbox/client.ts +75 -0
- package/src/outbox/decision.ts +209 -0
- package/src/outbox/delivery.ts +163 -0
- package/src/outbox/enforcement/executor.ts +288 -0
- package/src/outbox/enforcement/planner.ts +377 -0
- package/src/outbox/evidence.ts +231 -0
- package/src/outbox/inbound.ts +140 -0
- package/src/outbox/index.ts +160 -0
- package/src/outbox/intake.ts +175 -0
- package/src/outbox/integration.ts +205 -0
- package/src/outbox/outbox/dispatcher.ts +131 -0
- package/src/outbox/outbox/service.ts +444 -0
- package/src/outbox/postgres/index.ts +59 -0
- package/src/outbox/postgres/registries.ts +177 -0
- package/src/outbox/postgres/reportColumns.ts +281 -0
- package/src/outbox/postgres/store/enforcement.ts +186 -0
- package/src/outbox/postgres/store/events.ts +112 -0
- package/src/outbox/postgres/store/index.ts +72 -0
- package/src/outbox/postgres/store/outbox.ts +273 -0
- package/src/outbox/postgres/store/reports.ts +349 -0
- package/src/outbox/postgres/store/transaction.ts +64 -0
- package/src/outbox/postgres/tables.ts +339 -0
- package/src/outbox/reconciliation.ts +199 -0
- package/src/outbox/reportStatus.ts +24 -0
- package/src/outbox/retention.ts +34 -0
- package/src/outbox/store/types.ts +528 -0
- package/src/outbox/types.ts +753 -0
- package/src/outbox/webhook.ts +165 -0
- package/src/reports.ts +138 -0
- package/src/testing/fixtures.ts +183 -0
- package/src/testing/index.ts +57 -0
- package/src/testing/sandbox.ts +469 -0
- package/src/testing/webhook-simulator.ts +166 -0
- package/src/transport.ts +250 -0
- package/src/webhookEndpoints.ts +256 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@crowdsource.you/core/outbox` — the application half of a CrowdSource integration.
|
|
3
|
+
*
|
|
4
|
+
* An application that adopts CrowdSource has to solve the same six problems
|
|
5
|
+
* every other application does: store a report and its promise of delivery
|
|
6
|
+
* atomically, deliver it with retries and a dead-letter path, receive signed
|
|
7
|
+
* decisions without a body parser destroying the signature, deduplicate
|
|
8
|
+
* redeliveries across several tasks, apply a decision without a stale revision
|
|
9
|
+
* overwriting a fresh one, and carry out consequences exactly once and
|
|
10
|
+
* reversibly. None of that has anything to do with what the application's
|
|
11
|
+
* objects are.
|
|
12
|
+
*
|
|
13
|
+
* So all of it is here, and an application supplies four things: its subjects,
|
|
14
|
+
* its category mapping, its enforcement tables, and a STORE built by the
|
|
15
|
+
* PostgreSQL store factory.
|
|
16
|
+
*
|
|
17
|
+
* ```ts
|
|
18
|
+
* import { createModerationIntegration } from '@crowdsource.you/core/outbox';
|
|
19
|
+
* import { postgresModerationStore } from '@crowdsource.you/core/outbox/postgres';
|
|
20
|
+
*
|
|
21
|
+
* const moderation = createModerationIntegration({
|
|
22
|
+
* store,
|
|
23
|
+
* crowdSource: { enabled: true, serviceKey, webhookSecret, enforcementMode: 'observe' },
|
|
24
|
+
* subjects: [listingSubjectProvider(), reviewSubjectProvider()],
|
|
25
|
+
* taxonomy: { version: '2026.07', allegationsFor },
|
|
26
|
+
* enforcement: commerceEnforcement,
|
|
27
|
+
* logger,
|
|
28
|
+
* });
|
|
29
|
+
*
|
|
30
|
+
* // Indexes before the first write: the unique ones ARE the exactly-once
|
|
31
|
+
* // mechanism, and an index that does not exist yet refuses nothing.
|
|
32
|
+
* await store.ensureSchema();
|
|
33
|
+
*
|
|
34
|
+
* // BEFORE express.json() — the signature covers the bytes that arrived.
|
|
35
|
+
* app.use('/webhooks', moderation.webhookRouter());
|
|
36
|
+
* app.use(express.json());
|
|
37
|
+
*
|
|
38
|
+
* moderation.dispatcher.start();
|
|
39
|
+
* ```
|
|
40
|
+
*
|
|
41
|
+
* Two invariants are ENFORCED here rather than documented, because both fail
|
|
42
|
+
* silently and neither shows up in a test that only asserts the happy path:
|
|
43
|
+
*
|
|
44
|
+
* 1. Nothing can be enqueued that is not already recorded in the outbox, in the
|
|
45
|
+
* same transaction. {@link ModerationOutboxTransactionError} is thrown by the
|
|
46
|
+
* only writer of that collection when the transaction it was handed is not
|
|
47
|
+
* open.
|
|
48
|
+
* 2. The webhook receiver reads raw bytes. Mounted after a JSON parser it
|
|
49
|
+
* refuses rather than verifying a signature over a re-serialisation.
|
|
50
|
+
*
|
|
51
|
+
* Types come from `@crowdsource.you/contracts` and `@crowdsource.you/core`. This
|
|
52
|
+
* package re-exports none of them: a `Decision` or a `TaxonomyCode` has exactly
|
|
53
|
+
* one definition.
|
|
54
|
+
*/
|
|
55
|
+
export { createModerationIntegration } from './integration.js';
|
|
56
|
+
/**
|
|
57
|
+
* The retention windows are storage-independent policy, so they stay here while
|
|
58
|
+
* PostgreSQL-specific tables and stores live behind the `/postgres` subpath.
|
|
59
|
+
*/
|
|
60
|
+
export { MODERATION_EVENT_RETENTION_SECONDS, MODERATION_OUTBOX_RETENTION_SECONDS, } from './retention.js';
|
|
61
|
+
export { ModerationOutboxTransactionError, decisionApplyEventId, isRetryableDeliveryError, reportSubmitEventId, } from './outbox/service.js';
|
|
62
|
+
export { ModerationOutboxDispatcher } from './outbox/dispatcher.js';
|
|
63
|
+
export { ModerationReconciliationJob } from './reconciliation.js';
|
|
64
|
+
export { DuplicateReportError } from './intake.js';
|
|
65
|
+
export { CrowdSourceUnavailableError, ModerationDeliveryRejectedError, } from './delivery.js';
|
|
66
|
+
export { ModerationDecisionDeferredError, ModerationDecisionRejectedError, } from './decision.js';
|
|
67
|
+
export { ModerationSubjectUnsupportedError, ModerationTaxonomyError, createSubjectRegistry, snapshotHash, } from './evidence.js';
|
|
68
|
+
export { ModerationRestoreDirectionError, assertRestoreDirection, planEnforcement, primaryAction, } from './enforcement/planner.js';
|
|
69
|
+
export { localStatusForDecision } from './reportStatus.js';
|
|
70
|
+
export { createProcessedEventStore } from './inbound.js';
|
|
71
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/outbox/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqDG;AAEH,OAAO,EAAE,2BAA2B,EAAE,MAAM,kBAAkB,CAAC;AAG/D;;;GAGG;AACH,OAAO,EACL,kCAAkC,EAClC,mCAAmC,GACpC,MAAM,gBAAgB,CAAC;AAExB,OAAO,EACL,gCAAgC,EAChC,oBAAoB,EACpB,wBAAwB,EACxB,mBAAmB,GACpB,MAAM,qBAAqB,CAAC;AAsB7B,OAAO,EAAE,0BAA0B,EAAE,MAAM,wBAAwB,CAAC;AACpE,OAAO,EAAE,2BAA2B,EAAE,MAAM,qBAAqB,CAAC;AAGlE,OAAO,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAEnD,OAAO,EACL,2BAA2B,EAC3B,+BAA+B,GAChC,MAAM,eAAe,CAAC;AAEvB,OAAO,EACL,+BAA+B,EAC/B,+BAA+B,GAChC,MAAM,eAAe,CAAC;AAEvB,OAAO,EACL,iCAAiC,EACjC,uBAAuB,EACvB,qBAAqB,EACrB,YAAY,GACb,MAAM,eAAe,CAAC;AAGvB,OAAO,EACL,+BAA+B,EAC/B,sBAAsB,EACtB,eAAe,EACf,aAAa,GACd,MAAM,0BAA0B,CAAC;AAGlC,OAAO,EAAE,sBAAsB,EAAE,MAAM,mBAAmB,CAAC;AAE3D,OAAO,EAAE,yBAAyB,EAAE,MAAM,cAAc,CAAC"}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { reportSubmitEventId } from './outbox/service.js';
|
|
2
|
+
/**
|
|
3
|
+
* Storing a report and, when there is somewhere to send it, the promise to
|
|
4
|
+
* deliver it — in one operation.
|
|
5
|
+
*
|
|
6
|
+
* This is the only thing in the integration that a user waits for. A 201 from an
|
|
7
|
+
* application's report route means the report row and its outbox event committed
|
|
8
|
+
* together. It does NOT mean CrowdSource accepted anything — CrowdSource may be
|
|
9
|
+
* unreachable, mid-deploy or not yet configured, and the reporter is told their
|
|
10
|
+
* report was received either way, because it was.
|
|
11
|
+
*
|
|
12
|
+
* The transaction is the whole mechanism. Two writes outside one would give two
|
|
13
|
+
* failure modes that are both silent: a report with no delivery event (the
|
|
14
|
+
* report exists, nothing will ever send it, and nobody finds out until somebody
|
|
15
|
+
* asks why a case never opened) or a delivery event with no report (a delivery
|
|
16
|
+
* worker looking up an id that was rolled back). Neither surfaces as an error at
|
|
17
|
+
* the moment it happens, which is exactly why this has to be atomic rather than
|
|
18
|
+
* carefully ordered.
|
|
19
|
+
*
|
|
20
|
+
* The one report with NO delivery event is the one whose type has no subject
|
|
21
|
+
* provider, and that is a different claim entirely: not "delivery failed" but
|
|
22
|
+
* "there was never a route out of this application for this kind of object".
|
|
23
|
+
* Those two must not be conflated, which is why they are different `localStatus`
|
|
24
|
+
* values and why the absent route is written down as a reason rather than
|
|
25
|
+
* inferred from a missing row.
|
|
26
|
+
*/
|
|
27
|
+
export class DuplicateReportError extends Error {
|
|
28
|
+
constructor(existing) {
|
|
29
|
+
super('This item has already been reported by this reporter.');
|
|
30
|
+
this.name = 'DuplicateReportError';
|
|
31
|
+
this.existing = existing;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Refuses an identifier that is not a string, before it reaches the store.
|
|
36
|
+
*
|
|
37
|
+
* The input type says these are strings, but a type is erased at runtime and a
|
|
38
|
+
* truthiness check passes anything non-empty — including an object. What that
|
|
39
|
+
* costs depends on the backend, and the difference is worth stating rather than
|
|
40
|
+
* flattening: on Mongo a `{ $ne: null }` becomes a query OPERATOR, so the
|
|
41
|
+
* duplicate lookup matches an UNRELATED report and answers "you already reported
|
|
42
|
+
* this" about somebody else's row; on Postgres a bound parameter cannot become
|
|
43
|
+
* an operator, so that particular failure class does not exist there.
|
|
44
|
+
*
|
|
45
|
+
* The guard is not Mongo's, though. A non-string still reaches the insert and
|
|
46
|
+
* stores something that is not an id where an id belongs, on any backend — and
|
|
47
|
+
* the check lives here rather than at an application's route because this
|
|
48
|
+
* function is exported: a queue worker, a reconciliation script or a future
|
|
49
|
+
* admin path is under no obligation to have passed a route's validation, and a
|
|
50
|
+
* guard that only exists at one caller is a guard that holds until the second
|
|
51
|
+
* one arrives.
|
|
52
|
+
*/
|
|
53
|
+
function requireIdentifier(value, field) {
|
|
54
|
+
/**
|
|
55
|
+
* `trim()` and not `length === 0`, because `' '` is not an identifier.
|
|
56
|
+
*
|
|
57
|
+
* A whitespace-only reporter or reported id is not a string somebody meant:
|
|
58
|
+
* it comes from a form field, a trimmed-elsewhere value, or a client that
|
|
59
|
+
* sends `' '` for absent. Stored, it is an id that matches nothing and reads
|
|
60
|
+
* as present — the duplicate check finds no earlier report, the delivery
|
|
61
|
+
* carries it as `externalReportId`, and nothing fails until a human asks why
|
|
62
|
+
* a case names nobody.
|
|
63
|
+
*/
|
|
64
|
+
if (typeof value !== 'string' || value.trim() === '') {
|
|
65
|
+
throw new TypeError(`createReport: ${field} must be a non-empty string.`);
|
|
66
|
+
}
|
|
67
|
+
return value;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Why a report is not going anywhere, in words an operator can read.
|
|
71
|
+
*
|
|
72
|
+
* Stored on the row rather than left to be inferred from a missing outbox event.
|
|
73
|
+
* A missing row is also what a lost write looks like, and the two need to be
|
|
74
|
+
* distinguishable months later without re-deriving which types had providers at
|
|
75
|
+
* the time. Bounded by the schema's 300-character limit.
|
|
76
|
+
*/
|
|
77
|
+
function localOnlyReason(reportedType) {
|
|
78
|
+
return (`There is no moderation subject provider for '${reportedType}', so this report is ` +
|
|
79
|
+
'recorded locally and is not sent for community review.');
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Store the report, and queue its delivery in the same transaction.
|
|
83
|
+
*
|
|
84
|
+
* Delivery is queued when — and only when — the reported type has a subject
|
|
85
|
+
* provider. A type without one is stored at `received` with the reason recorded,
|
|
86
|
+
* which is the behaviour the application had before CrowdSource existed: the
|
|
87
|
+
* report is a receipt and a local record, and nothing else ever happens to it.
|
|
88
|
+
*
|
|
89
|
+
* That branch is the reason the two writes stay in one transaction rather than
|
|
90
|
+
* being ordered carefully. The condition is read BEFORE the transaction body
|
|
91
|
+
* decides anything, so `localStatus` and the presence of an outbox row are
|
|
92
|
+
* decided together from one fact — a report can never commit as `queued` with
|
|
93
|
+
* nothing to deliver it, nor as `received` with a delivery event that will try
|
|
94
|
+
* anyway.
|
|
95
|
+
*
|
|
96
|
+
* Intake deliberately does not read whether the integration is enabled. A report
|
|
97
|
+
* taken while it is off still gets its delivery event, so turning the flag on
|
|
98
|
+
* delivers the backlog instead of stranding it — the dispatcher is what is
|
|
99
|
+
* gated, not the durable record. Nothing here is conditional on a third party's
|
|
100
|
+
* state; only on whether this application knows how to describe the object at
|
|
101
|
+
* all.
|
|
102
|
+
*/
|
|
103
|
+
export function createIntake(input) {
|
|
104
|
+
return async (report) => {
|
|
105
|
+
const reporter = requireIdentifier(report.reporter, 'reporter');
|
|
106
|
+
const reportedId = requireIdentifier(report.reportedId, 'reportedId');
|
|
107
|
+
const reportedType = requireIdentifier(report.reportedType, 'reportedType');
|
|
108
|
+
if (!Array.isArray(report.categories) || report.categories.length === 0) {
|
|
109
|
+
throw new TypeError('createReport: categories must be a non-empty array.');
|
|
110
|
+
}
|
|
111
|
+
for (const category of report.categories) {
|
|
112
|
+
requireIdentifier(category, 'categories[]');
|
|
113
|
+
}
|
|
114
|
+
const deliverable = input.registry.providerFor(reportedType) !== undefined;
|
|
115
|
+
return await input.transaction.run(async (tx) => {
|
|
116
|
+
const existing = await input.reports.findDuplicate({ reporter, reportedId, reportedType }, tx);
|
|
117
|
+
if (existing)
|
|
118
|
+
throw new DuplicateReportError(existing);
|
|
119
|
+
const created = await input.reports.insert({
|
|
120
|
+
reportedType,
|
|
121
|
+
reportedId,
|
|
122
|
+
reporter,
|
|
123
|
+
categories: [...report.categories],
|
|
124
|
+
...(report.details === undefined ? {} : { details: report.details }),
|
|
125
|
+
localStatus: deliverable ? 'queued' : 'received',
|
|
126
|
+
...(deliverable ? {} : { localStatusReason: localOnlyReason(reportedType) }),
|
|
127
|
+
...(report.extra === undefined ? {} : { extra: report.extra }),
|
|
128
|
+
}, tx);
|
|
129
|
+
if (!deliverable)
|
|
130
|
+
return { report: created };
|
|
131
|
+
const outboxEventId = await input.outbox.enqueue({
|
|
132
|
+
eventId: reportSubmitEventId(created.id),
|
|
133
|
+
kind: 'report.submit',
|
|
134
|
+
payload: { reportId: created.id },
|
|
135
|
+
}, tx);
|
|
136
|
+
return { report: created, outboxEventId };
|
|
137
|
+
});
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
//# sourceMappingURL=intake.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"intake.js","sourceRoot":"","sources":["../../../src/outbox/intake.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,mBAAmB,EAAsB,MAAM,qBAAqB,CAAC;AAW9E;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,MAAM,OAAO,oBAA8B,SAAQ,KAAK;IAGtD,YAAY,QAAiB;QAC3B,KAAK,CAAC,uDAAuD,CAAC,CAAC;QAC/D,IAAI,CAAC,IAAI,GAAG,sBAAsB,CAAC;QACnC,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;IAC3B,CAAC;CACF;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,SAAS,iBAAiB,CAAC,KAAc,EAAE,KAAa;IACtD;;;;;;;;;OASG;IACH,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;QACrD,MAAM,IAAI,SAAS,CAAC,iBAAiB,KAAK,8BAA8B,CAAC,CAAC;IAC5E,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;;;;GAOG;AACH,SAAS,eAAe,CAAC,YAAoB;IAC3C,OAAO,CACL,gDAAgD,YAAY,uBAAuB;QACnF,wDAAwD,CACzD,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,UAAU,YAAY,CAA8C,KAKzE;IACC,OAAO,KAAK,EAAE,MAAM,EAAE,EAAE;QACtB,MAAM,QAAQ,GAAG,iBAAiB,CAAC,MAAM,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAC;QAChE,MAAM,UAAU,GAAG,iBAAiB,CAAC,MAAM,CAAC,UAAU,EAAE,YAAY,CAAC,CAAC;QACtE,MAAM,YAAY,GAAG,iBAAiB,CAAC,MAAM,CAAC,YAAY,EAAE,cAAc,CAAC,CAAC;QAC5E,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,UAAU,CAAC,IAAI,MAAM,CAAC,UAAU,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACxE,MAAM,IAAI,SAAS,CAAC,qDAAqD,CAAC,CAAC;QAC7E,CAAC;QACD,KAAK,MAAM,QAAQ,IAAI,MAAM,CAAC,UAAU,EAAE,CAAC;YACzC,iBAAiB,CAAC,QAAQ,EAAE,cAAc,CAAC,CAAC;QAC9C,CAAC;QACD,MAAM,WAAW,GAAG,KAAK,CAAC,QAAQ,CAAC,WAAW,CAAC,YAAY,CAAC,KAAK,SAAS,CAAC;QAE3E,OAAO,MAAM,KAAK,CAAC,WAAW,CAAC,GAAG,CAAC,KAAK,EAAE,EAAE,EAAE,EAAE;YAC9C,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,OAAO,CAAC,aAAa,CAChD,EAAE,QAAQ,EAAE,UAAU,EAAE,YAAY,EAAE,EACtC,EAAE,CACH,CAAC;YACF,IAAI,QAAQ;gBAAE,MAAM,IAAI,oBAAoB,CAAC,QAAQ,CAAC,CAAC;YAEvD,MAAM,OAAO,GAAG,MAAM,KAAK,CAAC,OAAO,CAAC,MAAM,CACxC;gBACE,YAAY;gBACZ,UAAU;gBACV,QAAQ;gBACR,UAAU,EAAE,CAAC,GAAG,MAAM,CAAC,UAAU,CAAC;gBAClC,GAAG,CAAC,MAAM,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,CAAC;gBACpE,WAAW,EAAE,WAAW,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,UAAU;gBAChD,GAAG,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,iBAAiB,EAAE,eAAe,CAAC,YAAY,CAAC,EAAE,CAAC;gBAC5E,GAAG,CAAC,MAAM,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,EAAE,CAAC;aAC/D,EACD,EAAE,CACH,CAAC;YAEF,IAAI,CAAC,WAAW;gBAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC;YAE7C,MAAM,aAAa,GAAG,MAAM,KAAK,CAAC,MAAM,CAAC,OAAO,CAC9C;gBACE,OAAO,EAAE,mBAAmB,CAAC,OAAO,CAAC,EAAE,CAAC;gBACxC,IAAI,EAAE,eAAe;gBACrB,OAAO,EAAE,EAAE,QAAQ,EAAE,OAAO,CAAC,EAAE,EAAE;aAClC,EACD,EAAE,CACH,CAAC;YAEF,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,aAAa,EAAE,CAAC;QAC5C,CAAC,CAAC,CAAC;IACL,CAAC,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { createClientProvider } from './client.js';
|
|
2
|
+
import { createDecisionWorker } from './decision.js';
|
|
3
|
+
import { createDeliveryWorker } from './delivery.js';
|
|
4
|
+
import { createEnforcementExecutor } from './enforcement/executor.js';
|
|
5
|
+
import { assertRestoreDirection } from './enforcement/planner.js';
|
|
6
|
+
import { createSubjectRegistry } from './evidence.js';
|
|
7
|
+
import { createInboundService, createProcessedEventStore } from './inbound.js';
|
|
8
|
+
import { ModerationOutboxDispatcher, createOutboxRouter } from './outbox/dispatcher.js';
|
|
9
|
+
import { createOutboxService } from './outbox/service.js';
|
|
10
|
+
import { createIntake } from './intake.js';
|
|
11
|
+
import { ModerationReconciliationJob, createReconciliation, } from './reconciliation.js';
|
|
12
|
+
import { createWebhookRouter } from './webhook.js';
|
|
13
|
+
export function createModerationIntegration(config) {
|
|
14
|
+
/**
|
|
15
|
+
* Refuse an inverted `restoreAction` before anything is wired. It cannot be
|
|
16
|
+
* caught by the type — both directions are `TAction[]` — and it does not fail
|
|
17
|
+
* at runtime; it applies a punishment on an accepted appeal.
|
|
18
|
+
*/
|
|
19
|
+
assertRestoreDirection(config.enforcement);
|
|
20
|
+
const store = config.store;
|
|
21
|
+
const outbox = createOutboxService({ store: store.outbox, logger: config.logger });
|
|
22
|
+
const registry = createSubjectRegistry(config.subjects);
|
|
23
|
+
const client = createClientProvider({
|
|
24
|
+
config: config.crowdSource,
|
|
25
|
+
logger: config.logger,
|
|
26
|
+
});
|
|
27
|
+
const enforcement = createEnforcementExecutor({
|
|
28
|
+
enforcement: store.enforcement,
|
|
29
|
+
config: config.enforcement,
|
|
30
|
+
defaultMode: config.crowdSource.enforcementMode,
|
|
31
|
+
logger: config.logger,
|
|
32
|
+
...(config.metrics === undefined ? {} : { metrics: config.metrics }),
|
|
33
|
+
});
|
|
34
|
+
const deliverReport = createDeliveryWorker({
|
|
35
|
+
reports: store.reports,
|
|
36
|
+
registry,
|
|
37
|
+
taxonomy: config.taxonomy,
|
|
38
|
+
client,
|
|
39
|
+
logger: config.logger,
|
|
40
|
+
...(config.metrics === undefined ? {} : { metrics: config.metrics }),
|
|
41
|
+
});
|
|
42
|
+
const applyDecision = createDecisionWorker({
|
|
43
|
+
reports: store.reports,
|
|
44
|
+
executor: enforcement,
|
|
45
|
+
enforcement: config.enforcement,
|
|
46
|
+
logger: config.logger,
|
|
47
|
+
...(config.reportDecisionExtraFields === undefined
|
|
48
|
+
? {}
|
|
49
|
+
: { reportDecisionExtraFields: config.reportDecisionExtraFields }),
|
|
50
|
+
});
|
|
51
|
+
const inbound = createInboundService({
|
|
52
|
+
transaction: store.transaction,
|
|
53
|
+
events: store.events,
|
|
54
|
+
outbox,
|
|
55
|
+
});
|
|
56
|
+
const reconcile = createReconciliation({
|
|
57
|
+
transaction: store.transaction,
|
|
58
|
+
reports: store.reports,
|
|
59
|
+
outbox,
|
|
60
|
+
logger: config.logger,
|
|
61
|
+
...(config.crowdSource.staleSubmittedHours === undefined
|
|
62
|
+
? {}
|
|
63
|
+
: { staleSubmittedHours: config.crowdSource.staleSubmittedHours }),
|
|
64
|
+
});
|
|
65
|
+
const dispatcher = new ModerationOutboxDispatcher({
|
|
66
|
+
outbox,
|
|
67
|
+
handler: createOutboxRouter({ deliverReport, applyDecision }),
|
|
68
|
+
logger: config.logger,
|
|
69
|
+
enabled: config.crowdSource.enabled,
|
|
70
|
+
...(config.crowdSource.outboxPollIntervalMs === undefined
|
|
71
|
+
? {}
|
|
72
|
+
: { pollIntervalMs: config.crowdSource.outboxPollIntervalMs }),
|
|
73
|
+
...(config.crowdSource.outboxBatchSize === undefined
|
|
74
|
+
? {}
|
|
75
|
+
: { batchSize: config.crowdSource.outboxBatchSize }),
|
|
76
|
+
});
|
|
77
|
+
const reconciliationJob = new ModerationReconciliationJob({
|
|
78
|
+
reconcile,
|
|
79
|
+
logger: config.logger,
|
|
80
|
+
enabled: config.crowdSource.enabled,
|
|
81
|
+
...(config.crowdSource.reconciliationIntervalMs === undefined
|
|
82
|
+
? {}
|
|
83
|
+
: { intervalMs: config.crowdSource.reconciliationIntervalMs }),
|
|
84
|
+
});
|
|
85
|
+
return {
|
|
86
|
+
createReport: createIntake({
|
|
87
|
+
transaction: store.transaction,
|
|
88
|
+
reports: store.reports,
|
|
89
|
+
registry,
|
|
90
|
+
outbox,
|
|
91
|
+
}),
|
|
92
|
+
webhookRouter(options = {}) {
|
|
93
|
+
return createWebhookRouter({
|
|
94
|
+
inbound,
|
|
95
|
+
store: createProcessedEventStore(store.events),
|
|
96
|
+
...(config.crowdSource.webhookSecret === undefined
|
|
97
|
+
? {}
|
|
98
|
+
: { secret: config.crowdSource.webhookSecret }),
|
|
99
|
+
...(config.crowdSource.webhookPreviousSecret === undefined
|
|
100
|
+
? {}
|
|
101
|
+
: { previousSecret: config.crowdSource.webhookPreviousSecret }),
|
|
102
|
+
logger: config.logger,
|
|
103
|
+
...(config.metrics === undefined ? {} : { metrics: config.metrics }),
|
|
104
|
+
...(options.path === undefined ? {} : { path: options.path }),
|
|
105
|
+
});
|
|
106
|
+
},
|
|
107
|
+
dispatcher,
|
|
108
|
+
reconciliationJob,
|
|
109
|
+
reconcile,
|
|
110
|
+
deliverableTypes: () => registry.deliverableTypes(),
|
|
111
|
+
registry,
|
|
112
|
+
enforcement,
|
|
113
|
+
client,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
//# sourceMappingURL=integration.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"integration.js","sourceRoot":"","sources":["../../../src/outbox/integration.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The loop that drains the moderation outbox.
|
|
3
|
+
*
|
|
4
|
+
* A bounded interval, one batch in flight at a time, an abort signal that stops
|
|
5
|
+
* claiming new work but lets the event already being handled reach a durable
|
|
6
|
+
* state.
|
|
7
|
+
*
|
|
8
|
+
* It is NOT leader-gated, and that is a property of the claim rather than an
|
|
9
|
+
* oversight. Every event is taken under a lease with an owner check, so N tasks
|
|
10
|
+
* draining the same collection simply share the work — and a task dying
|
|
11
|
+
* mid-delivery has its lease expire and its event reclaimed, which a single
|
|
12
|
+
* leader would not give us.
|
|
13
|
+
*
|
|
14
|
+
* `enabled` gates the LOOP, never the durable record. Reports taken while the
|
|
15
|
+
* integration is off keep their outbox rows and deliver when it is switched on;
|
|
16
|
+
* running the loop instead would count attempts against a deployment that has
|
|
17
|
+
* nowhere to send anything and dead-letter the backlog it was supposed to
|
|
18
|
+
* preserve.
|
|
19
|
+
*/
|
|
20
|
+
const DEFAULT_POLL_INTERVAL_MS = 5000;
|
|
21
|
+
export class ModerationOutboxDispatcher {
|
|
22
|
+
constructor(options) {
|
|
23
|
+
this.options = options;
|
|
24
|
+
this.timer = null;
|
|
25
|
+
this.inFlight = null;
|
|
26
|
+
this.abortController = null;
|
|
27
|
+
this.running = false;
|
|
28
|
+
}
|
|
29
|
+
start() {
|
|
30
|
+
if (this.running)
|
|
31
|
+
return;
|
|
32
|
+
if (!this.options.enabled) {
|
|
33
|
+
this.options.logger.info('[CrowdSource] outbox dispatcher not started: the integration is disabled');
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const intervalMs = this.options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
37
|
+
this.running = true;
|
|
38
|
+
this.abortController = new AbortController();
|
|
39
|
+
void this.tick();
|
|
40
|
+
this.timer = setInterval(() => {
|
|
41
|
+
void this.tick();
|
|
42
|
+
}, intervalMs);
|
|
43
|
+
this.timer.unref?.();
|
|
44
|
+
this.options.logger.info('[CrowdSource] outbox dispatcher started', {
|
|
45
|
+
intervalMs,
|
|
46
|
+
batchSize: this.options.batchSize,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
async stop() {
|
|
50
|
+
this.running = false;
|
|
51
|
+
const controller = this.abortController;
|
|
52
|
+
controller?.abort();
|
|
53
|
+
if (this.timer) {
|
|
54
|
+
clearInterval(this.timer);
|
|
55
|
+
this.timer = null;
|
|
56
|
+
}
|
|
57
|
+
await this.inFlight;
|
|
58
|
+
if (this.abortController === controller) {
|
|
59
|
+
this.abortController = null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
async tick() {
|
|
63
|
+
if (!this.running)
|
|
64
|
+
return;
|
|
65
|
+
if (this.inFlight)
|
|
66
|
+
return this.inFlight;
|
|
67
|
+
const work = this.options.outbox
|
|
68
|
+
.dispatch({
|
|
69
|
+
handler: this.options.handler,
|
|
70
|
+
...(this.options.batchSize === undefined
|
|
71
|
+
? {}
|
|
72
|
+
: { batchSize: this.options.batchSize }),
|
|
73
|
+
...(this.abortController === null
|
|
74
|
+
? {}
|
|
75
|
+
: { signal: this.abortController.signal }),
|
|
76
|
+
})
|
|
77
|
+
.then(({ processed, failed, deadLettered }) => {
|
|
78
|
+
if (processed > 0 || failed > 0) {
|
|
79
|
+
this.options.logger.info('[CrowdSource] outbox batch complete', {
|
|
80
|
+
processed,
|
|
81
|
+
failed,
|
|
82
|
+
deadLettered,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
})
|
|
86
|
+
.catch((error) => {
|
|
87
|
+
// Claim/database failures happen outside the per-event retry block. Keep
|
|
88
|
+
// the interval alive and avoid an unhandled rejection.
|
|
89
|
+
this.options.logger.error('[CrowdSource] outbox tick failed', {
|
|
90
|
+
error: error instanceof Error ? error.message : String(error),
|
|
91
|
+
});
|
|
92
|
+
})
|
|
93
|
+
.finally(() => {
|
|
94
|
+
if (this.inFlight === work)
|
|
95
|
+
this.inFlight = null;
|
|
96
|
+
});
|
|
97
|
+
this.inFlight = work;
|
|
98
|
+
return work;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/** Route an event to the worker that owns its kind. */
|
|
102
|
+
export function createOutboxRouter(handlers) {
|
|
103
|
+
return async (event) => {
|
|
104
|
+
switch (event.kind) {
|
|
105
|
+
case 'report.submit':
|
|
106
|
+
await handlers.deliverReport(event);
|
|
107
|
+
return;
|
|
108
|
+
case 'decision.apply':
|
|
109
|
+
await handlers.applyDecision(event);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
//# sourceMappingURL=dispatcher.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dispatcher.js","sourceRoot":"","sources":["../../../../src/outbox/outbox/dispatcher.ts"],"names":[],"mappings":"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"}
|