@econ-v1/domain 6.7.0 → 6.10.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/errors/index.d.ts +96 -25
- package/dist/errors/index.d.ts.map +1 -1
- package/dist/events/index.d.ts +127 -20
- package/dist/events/index.d.ts.map +1 -1
- package/dist/index.d.ts +250 -47
- package/dist/index.d.ts.map +1 -1
- package/dist/models/agent.d.ts +25 -4
- package/dist/models/agent.d.ts.map +1 -1
- package/dist/models/app-data.d.ts +84 -0
- package/dist/models/app-data.d.ts.map +1 -0
- package/dist/models/conversation.d.ts +12 -2
- package/dist/models/conversation.d.ts.map +1 -1
- package/dist/models/delegation.d.ts +12 -3
- package/dist/models/delegation.d.ts.map +1 -1
- package/dist/models/device-boot-state.d.ts +12 -0
- package/dist/models/device-boot-state.d.ts.map +1 -0
- package/dist/models/device-identity.d.ts +11 -3
- package/dist/models/device-identity.d.ts.map +1 -1
- package/dist/models/graph.d.ts +26 -3
- package/dist/models/graph.d.ts.map +1 -1
- package/dist/models/index.d.ts +82 -14
- package/dist/models/index.d.ts.map +1 -1
- package/dist/models/json.d.ts +5 -5
- package/dist/models/json.d.ts.map +1 -1
- package/dist/models/l402.d.ts +4 -5
- package/dist/models/l402.d.ts.map +1 -1
- package/dist/models/live-query-definition.d.ts +15 -16
- package/dist/models/live-query-definition.d.ts.map +1 -1
- package/dist/models/node-app-memory.d.ts +107 -0
- package/dist/models/node-app-memory.d.ts.map +1 -0
- package/dist/models/outbox-intent.d.ts +3 -3
- package/dist/models/outbox-intent.d.ts.map +1 -1
- package/dist/models/outbox.d.ts +67 -15
- package/dist/models/outbox.d.ts.map +1 -1
- package/dist/models/owner-views.d.ts +28 -28
- package/dist/models/owner-views.d.ts.map +1 -1
- package/dist/models/payment.d.ts +28 -9
- package/dist/models/payment.d.ts.map +1 -1
- package/dist/models/query-diff.d.ts +2 -2
- package/dist/models/query-diff.d.ts.map +1 -1
- package/dist/models/release-manifest.d.ts +32 -47
- package/dist/models/release-manifest.d.ts.map +1 -1
- package/dist/models/repository-write.d.ts +3 -3
- package/dist/models/repository-write.d.ts.map +1 -1
- package/dist/models/revisioned-commands.d.ts +66 -13
- package/dist/models/revisioned-commands.d.ts.map +1 -1
- package/dist/models/revisioned.d.ts +50 -10
- package/dist/models/revisioned.d.ts.map +1 -1
- package/dist/models/settings.d.ts +2 -2
- package/dist/models/settings.d.ts.map +1 -1
- package/dist/models/sync.d.ts +18 -18
- package/dist/models/sync.d.ts.map +1 -1
- package/dist/models/wallet.d.ts +16 -16
- package/dist/models/wallet.d.ts.map +1 -1
- package/dist/queries/certificate-trust.d.ts +13 -0
- package/dist/queries/certificate-trust.d.ts.map +1 -0
- package/dist/queries/conversations.d.ts +38 -7
- package/dist/queries/conversations.d.ts.map +1 -1
- package/dist/queries/discovery.d.ts +10 -10
- package/dist/queries/discovery.d.ts.map +1 -1
- package/dist/queries/notifications.d.ts +24 -4
- package/dist/queries/notifications.d.ts.map +1 -1
- package/dist/queries/onboarding-flow.d.ts +25 -0
- package/dist/queries/onboarding-flow.d.ts.map +1 -0
- package/dist/queries/onboarding-lcd-screen.d.ts +56 -0
- package/dist/queries/onboarding-lcd-screen.d.ts.map +1 -0
- package/dist/queries/onboarding-status.d.ts +10 -10
- package/dist/queries/onboarding-status.d.ts.map +1 -1
- package/dist/queries/revisioned.d.ts +47 -13
- package/dist/queries/revisioned.d.ts.map +1 -1
- package/dist/queries/wallet.d.ts +40 -10
- package/dist/queries/wallet.d.ts.map +1 -1
- package/dist/services/abort-signal.d.ts +54 -0
- package/dist/services/abort-signal.d.ts.map +1 -0
- package/dist/services/certificate-trust-service.d.ts +42 -0
- package/dist/services/certificate-trust-service.d.ts.map +1 -0
- package/dist/services/conversation-service.d.ts +103 -15
- package/dist/services/conversation-service.d.ts.map +1 -1
- package/dist/services/conversation-sync.d.ts +9 -5
- package/dist/services/conversation-sync.d.ts.map +1 -1
- package/dist/services/device-boot-state-service.d.ts +17 -0
- package/dist/services/device-boot-state-service.d.ts.map +1 -0
- package/dist/services/device-cleanup-service.d.ts +47 -0
- package/dist/services/device-cleanup-service.d.ts.map +1 -0
- package/dist/services/discovery-probe-service.d.ts +37 -46
- package/dist/services/discovery-probe-service.d.ts.map +1 -1
- package/dist/services/gossip-service.d.ts +28 -13
- package/dist/services/gossip-service.d.ts.map +1 -1
- package/dist/services/invite-redemption-service.d.ts +43 -0
- package/dist/services/invite-redemption-service.d.ts.map +1 -0
- package/dist/services/lan-candidate-refresh-service.d.ts +22 -7
- package/dist/services/lan-candidate-refresh-service.d.ts.map +1 -1
- package/dist/services/lcd-decision-service.d.ts +58 -0
- package/dist/services/lcd-decision-service.d.ts.map +1 -0
- package/dist/services/onboarding-checkpoint-store.d.ts +54 -0
- package/dist/services/onboarding-checkpoint-store.d.ts.map +1 -0
- package/dist/services/onboarding-flow-service.d.ts +180 -0
- package/dist/services/onboarding-flow-service.d.ts.map +1 -0
- package/dist/services/onboarding-status-service.d.ts +21 -38
- package/dist/services/onboarding-status-service.d.ts.map +1 -1
- package/dist/services/outbox-service.d.ts +105 -29
- package/dist/services/outbox-service.d.ts.map +1 -1
- package/dist/services/owner-claim-service.d.ts +187 -0
- package/dist/services/owner-claim-service.d.ts.map +1 -0
- package/dist/services/owner-view-refresh-service.d.ts +40 -0
- package/dist/services/owner-view-refresh-service.d.ts.map +1 -0
- package/dist/services/owner-view-service.d.ts +49 -4
- package/dist/services/owner-view-service.d.ts.map +1 -1
- package/dist/services/peer-directory-service.d.ts +53 -17
- package/dist/services/peer-directory-service.d.ts.map +1 -1
- package/dist/services/release-manifest-verifier.d.ts +18 -20
- package/dist/services/release-manifest-verifier.d.ts.map +1 -1
- package/dist/services/revisioned-domain-service.d.ts +50 -17
- package/dist/services/revisioned-domain-service.d.ts.map +1 -1
- package/dist/services/runtime-globals.d.ts +39 -0
- package/dist/services/runtime-globals.d.ts.map +1 -0
- package/dist/services/session-service.d.ts +23 -3
- package/dist/services/session-service.d.ts.map +1 -1
- package/dist/services/sync/authoritative-mirror.d.ts +88 -15
- package/dist/services/sync/authoritative-mirror.d.ts.map +1 -1
- package/dist/services/sync/index.d.ts +17 -5
- package/dist/services/sync/index.d.ts.map +1 -1
- package/dist/services/sync/log-sync.d.ts +42 -8
- package/dist/services/sync/log-sync.d.ts.map +1 -1
- package/dist/services/sync/revision-merge.d.ts +80 -13
- package/dist/services/sync/revision-merge.d.ts.map +1 -1
- package/dist/services/sync/strategy.d.ts +11 -1
- package/dist/services/sync/strategy.d.ts.map +1 -1
- package/dist/services/transport-manager.d.ts +84 -29
- package/dist/services/transport-manager.d.ts.map +1 -1
- package/dist/services/wallet-mirror-service.d.ts +81 -18
- package/dist/services/wallet-mirror-service.d.ts.map +1 -1
- package/dist/services/wifi-provisioning-service.d.ts +146 -0
- package/dist/services/wifi-provisioning-service.d.ts.map +1 -0
- package/package.json +18 -12
- package/src/errors/index.js +126 -0
- package/src/events/index.js +124 -0
- package/src/index.js +566 -0
- package/src/models/agent.js +43 -0
- package/src/models/app-data.js +122 -0
- package/src/models/conversation.js +36 -0
- package/{dist → src}/models/delegation.js +18 -1
- package/src/models/device-boot-state.js +3 -0
- package/{dist → src}/models/device-identity.js +18 -2
- package/{dist → src}/models/graph.js +30 -3
- package/src/models/index.js +175 -0
- package/src/models/json.js +18 -0
- package/src/models/l402.js +9 -0
- package/src/models/live-query-definition.js +30 -0
- package/src/models/node-app-memory.js +130 -0
- package/src/models/outbox-intent.js +12 -0
- package/{dist → src}/models/outbox.js +102 -3
- package/src/models/owner-views.js +59 -0
- package/src/models/payment.js +24 -0
- package/src/models/query-diff.js +7 -0
- package/src/models/release-manifest.js +160 -0
- package/src/models/repository-write.js +10 -0
- package/src/models/revisioned-commands.js +92 -0
- package/src/models/revisioned.js +41 -0
- package/src/models/settings.js +16 -0
- package/src/models/sync.js +44 -0
- package/src/models/wallet.js +40 -0
- package/src/queries/certificate-trust.js +31 -0
- package/{dist → src}/queries/conversations.js +54 -3
- package/src/queries/discovery.js +34 -0
- package/{dist → src}/queries/notifications.js +25 -2
- package/src/queries/onboarding-flow.js +48 -0
- package/src/queries/onboarding-lcd-screen.js +100 -0
- package/src/queries/onboarding-status.js +34 -0
- package/src/queries/revisioned.js +128 -0
- package/{dist → src}/queries/wallet.js +35 -1
- package/src/services/abort-signal.js +69 -0
- package/src/services/certificate-trust-service.js +119 -0
- package/{dist → src}/services/conversation-service.js +108 -1
- package/src/services/conversation-sync.js +28 -0
- package/src/services/device-boot-state-service.js +45 -0
- package/src/services/device-cleanup-service.js +112 -0
- package/src/services/discovery-probe-service.js +196 -0
- package/{dist → src}/services/gossip-service.js +54 -4
- package/src/services/invite-redemption-service.js +64 -0
- package/src/services/lan-candidate-refresh-service.js +164 -0
- package/src/services/lcd-decision-service.js +93 -0
- package/src/services/onboarding-checkpoint-store.js +155 -0
- package/src/services/onboarding-flow-service.js +1096 -0
- package/src/services/onboarding-status-service.js +64 -0
- package/{dist → src}/services/outbox-service.js +132 -1
- package/src/services/owner-claim-service.js +347 -0
- package/src/services/owner-view-refresh-service.js +208 -0
- package/src/services/owner-view-service.js +87 -0
- package/{dist → src}/services/peer-directory-service.js +72 -3
- package/src/services/release-manifest-verifier.js +108 -0
- package/{dist → src}/services/revisioned-domain-service.js +189 -9
- package/src/services/runtime-globals.js +174 -0
- package/src/services/session-service.js +50 -0
- package/{dist → src}/services/sync/authoritative-mirror.js +90 -1
- package/src/services/sync/index.js +67 -0
- package/src/services/sync/log-sync.js +60 -0
- package/src/services/sync/revision-merge.js +119 -0
- package/src/services/sync/strategy.js +44 -0
- package/src/services/transport-manager.js +478 -0
- package/{dist → src}/services/wallet-mirror-service.js +226 -6
- package/src/services/wifi-provisioning-service.js +278 -0
- package/dist/errors/index.js +0 -45
- package/dist/errors/index.js.map +0 -1
- package/dist/events/index.js +0 -17
- package/dist/events/index.js.map +0 -1
- package/dist/index.js +0 -31
- package/dist/index.js.map +0 -1
- package/dist/models/agent.js +0 -17
- package/dist/models/agent.js.map +0 -1
- package/dist/models/conversation.js +0 -2
- package/dist/models/conversation.js.map +0 -1
- package/dist/models/delegation.js.map +0 -1
- package/dist/models/device-identity.js.map +0 -1
- package/dist/models/graph.js.map +0 -1
- package/dist/models/index.js +0 -8
- package/dist/models/index.js.map +0 -1
- package/dist/models/json.js +0 -2
- package/dist/models/json.js.map +0 -1
- package/dist/models/l402.js +0 -2
- package/dist/models/l402.js.map +0 -1
- package/dist/models/live-query-definition.js +0 -2
- package/dist/models/live-query-definition.js.map +0 -1
- package/dist/models/outbox-intent.js +0 -2
- package/dist/models/outbox-intent.js.map +0 -1
- package/dist/models/outbox.js.map +0 -1
- package/dist/models/owner-views.js +0 -7
- package/dist/models/owner-views.js.map +0 -1
- package/dist/models/payment.js +0 -3
- package/dist/models/payment.js.map +0 -1
- package/dist/models/query-diff.js +0 -2
- package/dist/models/query-diff.js.map +0 -1
- package/dist/models/release-manifest.js +0 -147
- package/dist/models/release-manifest.js.map +0 -1
- package/dist/models/repository-write.js +0 -2
- package/dist/models/repository-write.js.map +0 -1
- package/dist/models/revisioned-commands.js +0 -35
- package/dist/models/revisioned-commands.js.map +0 -1
- package/dist/models/revisioned.js +0 -2
- package/dist/models/revisioned.js.map +0 -1
- package/dist/models/settings.js +0 -2
- package/dist/models/settings.js.map +0 -1
- package/dist/models/sync.js +0 -2
- package/dist/models/sync.js.map +0 -1
- package/dist/models/wallet.js +0 -2
- package/dist/models/wallet.js.map +0 -1
- package/dist/queries/conversations.js.map +0 -1
- package/dist/queries/discovery.js +0 -27
- package/dist/queries/discovery.js.map +0 -1
- package/dist/queries/notifications.js.map +0 -1
- package/dist/queries/onboarding-status.js +0 -27
- package/dist/queries/onboarding-status.js.map +0 -1
- package/dist/queries/revisioned.js +0 -58
- package/dist/queries/revisioned.js.map +0 -1
- package/dist/queries/wallet.js.map +0 -1
- package/dist/services/conversation-service.js.map +0 -1
- package/dist/services/conversation-sync.js +0 -9
- package/dist/services/conversation-sync.js.map +0 -1
- package/dist/services/discovery-probe-service.js +0 -129
- package/dist/services/discovery-probe-service.js.map +0 -1
- package/dist/services/gossip-service.js.map +0 -1
- package/dist/services/lan-candidate-refresh-service.js +0 -79
- package/dist/services/lan-candidate-refresh-service.js.map +0 -1
- package/dist/services/onboarding-status-service.js +0 -32
- package/dist/services/onboarding-status-service.js.map +0 -1
- package/dist/services/outbox-service.js.map +0 -1
- package/dist/services/owner-view-service.js +0 -21
- package/dist/services/owner-view-service.js.map +0 -1
- package/dist/services/peer-directory-service.js.map +0 -1
- package/dist/services/release-manifest-verifier.js +0 -84
- package/dist/services/release-manifest-verifier.js.map +0 -1
- package/dist/services/revisioned-domain-service.js.map +0 -1
- package/dist/services/session-service.js +0 -20
- package/dist/services/session-service.js.map +0 -1
- package/dist/services/sync/authoritative-mirror.js.map +0 -1
- package/dist/services/sync/index.js +0 -4
- package/dist/services/sync/index.js.map +0 -1
- package/dist/services/sync/log-sync.js +0 -20
- package/dist/services/sync/log-sync.js.map +0 -1
- package/dist/services/sync/revision-merge.js +0 -46
- package/dist/services/sync/revision-merge.js.map +0 -1
- package/dist/services/sync/strategy.js +0 -2
- package/dist/services/sync/strategy.js.map +0 -1
- package/dist/services/transport-manager.js +0 -234
- package/dist/services/transport-manager.js.map +0 -1
- package/dist/services/wallet-mirror-service.js.map +0 -1
|
@@ -1,18 +1,108 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @typedef {import("@econ-v1/ports").OutboxRecord} OutboxRecord
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* @typedef {import("@econ-v1/ports").OutboxRepository} OutboxRepository
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* @typedef {import("@econ-v1/ports").RepositoryJsonValue} RepositoryJsonValue
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* @template {JsonValue} TValue
|
|
14
|
+
* @typedef {import("../models/revisioned.js").RevisionCommand<TValue>} RevisionCommand
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* @template TValue
|
|
18
|
+
* @typedef {import("../models/revisioned.js").RevisionCommandResult<TValue>} RevisionCommandResult
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* @template TValue
|
|
22
|
+
* @typedef {import("../models/revisioned.js").RevisionConflict<TValue>} RevisionConflict
|
|
23
|
+
*/
|
|
24
|
+
/**
|
|
25
|
+
* @template {JsonValue} TValue
|
|
26
|
+
* @typedef {import("../models/revisioned.js").RevisionConflictRecord<TValue>} RevisionConflictRecord
|
|
27
|
+
*/
|
|
28
|
+
/**
|
|
29
|
+
* @template {JsonValue} TValue
|
|
30
|
+
* @typedef {import("../models/revisioned.js").RevisionConflictPayload<TValue>} RevisionConflictPayload
|
|
31
|
+
*/
|
|
32
|
+
/**
|
|
33
|
+
* @template TValue
|
|
34
|
+
* @typedef {import("../models/revisioned.js").RevisionEntity<TValue>} RevisionEntity
|
|
35
|
+
*/
|
|
36
|
+
/**
|
|
37
|
+
* @typedef {import("../models/revisioned.js").RevisionResolution} RevisionResolution
|
|
38
|
+
*/
|
|
39
|
+
/**
|
|
40
|
+
* @typedef {import("../models/revisioned.js").RevisionedDomain} RevisionedDomain
|
|
41
|
+
*/
|
|
42
|
+
/**
|
|
43
|
+
* @typedef {import("../models/json.js").JsonValue} JsonValue
|
|
44
|
+
*/
|
|
45
|
+
/**
|
|
46
|
+
* @template {JsonValue} TValue
|
|
47
|
+
* @typedef {{ apply(entity: RevisionEntity<TValue>): Promise<void>; command(input: RevisionCommand<TValue>): Promise<RevisionCommandResult<TValue>>; read(id: string): Promise<RevisionEntity<TValue> | undefined>; }} RevisionedDomainRepository
|
|
48
|
+
*/
|
|
49
|
+
/**
|
|
50
|
+
* @template {JsonValue} TValue
|
|
51
|
+
* @typedef {{ readonly domain: RevisionedDomain; readonly now?: () => string; readonly outbox: OutboxRepository; readonly repository: RevisionedDomainRepository<TValue>; }} RevisionedDomainServiceOptions
|
|
52
|
+
*/
|
|
53
|
+
/**
|
|
54
|
+
* @template {JsonValue} TValue
|
|
55
|
+
* @typedef {| { readonly entity: RevisionEntity<TValue>; readonly status: "accepted" } | { readonly conflict: RevisionConflictRecord<TValue>; readonly status: "conflict" }} RevisionedWriteResult
|
|
56
|
+
*/
|
|
57
|
+
/**
|
|
58
|
+
* @template {JsonValue} TValue
|
|
59
|
+
* @typedef {| { readonly entity: RevisionEntity<TValue>; readonly status: "accepted" } | { readonly entity: RevisionEntity<TValue>; readonly status: "discarded" } | { readonly conflict: RevisionConflictRecord<TValue>; readonly status: "conflict" }} RevisionResolutionResult
|
|
60
|
+
*/
|
|
61
|
+
/**
|
|
62
|
+
* @template {JsonValue} TValue
|
|
63
|
+
* @typedef {{ readonly baseRev?: number; readonly id: string; readonly idempotencyKey: string; readonly value: TValue; }} RevisionedWriteInput
|
|
64
|
+
*/
|
|
1
65
|
import { DomainError } from "../errors/index.js";
|
|
2
66
|
import { createOutboxIntent } from "../models/outbox.js";
|
|
3
67
|
import { REVISIONED_DOMAINS, } from "../models/revisioned.js";
|
|
4
68
|
import { RevisionMerge } from "./sync/revision-merge.js";
|
|
69
|
+
/**
|
|
70
|
+
* @template {JsonValue} TValue
|
|
71
|
+
*/
|
|
5
72
|
export class RevisionedDomainService {
|
|
73
|
+
/**
|
|
74
|
+
* @readonly
|
|
75
|
+
* @type {RevisionedDomain}
|
|
76
|
+
*/
|
|
6
77
|
#domain;
|
|
78
|
+
/**
|
|
79
|
+
* @readonly
|
|
80
|
+
* @type {() => string}
|
|
81
|
+
*/
|
|
7
82
|
#now;
|
|
83
|
+
/**
|
|
84
|
+
* @readonly
|
|
85
|
+
* @type {OutboxRepository}
|
|
86
|
+
*/
|
|
8
87
|
#outbox;
|
|
88
|
+
/**
|
|
89
|
+
* @readonly
|
|
90
|
+
* @type {RevisionedDomainRepository<TValue>}
|
|
91
|
+
*/
|
|
9
92
|
#repository;
|
|
93
|
+
/**
|
|
94
|
+
* @param {RevisionedDomainServiceOptions<TValue>} options
|
|
95
|
+
*/
|
|
10
96
|
constructor(options) {
|
|
11
97
|
this.#domain = options.domain;
|
|
12
98
|
this.#now = options.now ?? (() => new Date().toISOString());
|
|
13
99
|
this.#outbox = options.outbox;
|
|
14
100
|
this.#repository = options.repository;
|
|
15
101
|
}
|
|
102
|
+
/**
|
|
103
|
+
* @param {RevisionedWriteInput<TValue>} input
|
|
104
|
+
* @returns {Promise<RevisionedWriteResult<TValue>>}
|
|
105
|
+
*/
|
|
16
106
|
async update(input) {
|
|
17
107
|
validateWriteInput(this.#domain, input);
|
|
18
108
|
const merge = this.#merge(input.idempotencyKey);
|
|
@@ -28,17 +118,30 @@ export class RevisionedDomainService {
|
|
|
28
118
|
throw new Error("Revision conflict was not persisted");
|
|
29
119
|
return { conflict, status: "conflict" };
|
|
30
120
|
}
|
|
121
|
+
/**
|
|
122
|
+
* @returns {Promise<readonly RevisionConflictRecord<TValue>[]>}
|
|
123
|
+
*/
|
|
31
124
|
async listConflicts() {
|
|
32
125
|
const records = await this.#outbox.list({ domain: this.#outboxDomain(), state: "conflict" });
|
|
33
|
-
return records.map((record) =>
|
|
126
|
+
return records.map((record) =>
|
|
127
|
+
/** @type {RevisionConflictRecord<TValue>} */ (
|
|
128
|
+
parseRevisionConflictRecord(record.payload, this.#domain)
|
|
129
|
+
));
|
|
34
130
|
}
|
|
131
|
+
/**
|
|
132
|
+
* @param {string} conflictIdValue
|
|
133
|
+
* @param {RevisionResolution} resolution
|
|
134
|
+
* @returns {Promise<RevisionResolutionResult<TValue>>}
|
|
135
|
+
*/
|
|
35
136
|
async resolveConflict(conflictIdValue, resolution) {
|
|
36
137
|
if (conflictIdValue.trim() === "")
|
|
37
138
|
throw invalid("conflict id");
|
|
38
139
|
const stored = await this.#outbox.read(conflictIdValue);
|
|
39
140
|
if (stored === undefined)
|
|
40
141
|
throw new DomainError({ code: "not_found", message: "Revision conflict was not found" });
|
|
41
|
-
const conflict =
|
|
142
|
+
const conflict = /** @type {RevisionConflictRecord<TValue>} */ (
|
|
143
|
+
parseRevisionConflictRecord(stored.payload, this.#domain)
|
|
144
|
+
);
|
|
42
145
|
if (resolution === "theirs") {
|
|
43
146
|
await this.#repository.apply(conflict.theirs);
|
|
44
147
|
await this.#outbox.delete(stored.id);
|
|
@@ -66,6 +169,10 @@ export class RevisionedDomainService {
|
|
|
66
169
|
await this.#outbox.delete(stored.id);
|
|
67
170
|
return { conflict: replacement, status: "conflict" };
|
|
68
171
|
}
|
|
172
|
+
/**
|
|
173
|
+
* @param {string} idempotencyKey
|
|
174
|
+
* @returns {RevisionMerge<TValue>}
|
|
175
|
+
*/
|
|
69
176
|
#merge(idempotencyKey) {
|
|
70
177
|
return new RevisionMerge({
|
|
71
178
|
parkConflict: async (conflict) => {
|
|
@@ -75,11 +182,18 @@ export class RevisionedDomainService {
|
|
|
75
182
|
write: async (entity) => await this.#repository.apply(entity),
|
|
76
183
|
});
|
|
77
184
|
}
|
|
185
|
+
/**
|
|
186
|
+
* @param {RevisionConflict<TValue> & { readonly idempotencyKey: string }} conflict
|
|
187
|
+
* @returns {Promise<RevisionConflictRecord<TValue>>}
|
|
188
|
+
*/
|
|
78
189
|
async #persistConflict(conflict) {
|
|
79
190
|
const id = conflictId(this.#domain, conflict.id, conflict.idempotencyKey);
|
|
80
191
|
const existing = await this.#outbox.read(id);
|
|
81
192
|
if (existing !== undefined)
|
|
82
|
-
return
|
|
193
|
+
return /** @type {RevisionConflictRecord<TValue>} */ (
|
|
194
|
+
parseRevisionConflictRecord(existing.payload, this.#domain)
|
|
195
|
+
);
|
|
196
|
+
/** @type {RevisionConflictPayload<TValue>} */
|
|
83
197
|
const record = {
|
|
84
198
|
...conflict,
|
|
85
199
|
conflictId: id,
|
|
@@ -87,6 +201,7 @@ export class RevisionedDomainService {
|
|
|
87
201
|
domain: this.#domain,
|
|
88
202
|
kind: "revision-conflict.v1",
|
|
89
203
|
};
|
|
204
|
+
/** @type {OutboxRecord} */
|
|
90
205
|
const entry = {
|
|
91
206
|
...createOutboxIntent({
|
|
92
207
|
authority: "eventual",
|
|
@@ -94,7 +209,7 @@ export class RevisionedDomainService {
|
|
|
94
209
|
domain: this.#outboxDomain(),
|
|
95
210
|
id,
|
|
96
211
|
idempotencyKey: `conflict:${id}`,
|
|
97
|
-
payload: record,
|
|
212
|
+
payload: /** @type {RepositoryJsonValue} */ (record),
|
|
98
213
|
state: "conflict",
|
|
99
214
|
updatedAt: record.createdAt,
|
|
100
215
|
}),
|
|
@@ -103,14 +218,31 @@ export class RevisionedDomainService {
|
|
|
103
218
|
await this.#outbox.create(entry);
|
|
104
219
|
return record;
|
|
105
220
|
}
|
|
221
|
+
/**
|
|
222
|
+
* @param {string} id
|
|
223
|
+
* @returns {Promise<RevisionConflictRecord<TValue> | undefined>}
|
|
224
|
+
*/
|
|
106
225
|
async #readConflict(id) {
|
|
107
226
|
const record = await this.#outbox.read(id);
|
|
108
|
-
return record === undefined
|
|
227
|
+
return record === undefined
|
|
228
|
+
? undefined
|
|
229
|
+
: /** @type {RevisionConflictRecord<TValue>} */ (
|
|
230
|
+
parseRevisionConflictRecord(record.payload, this.#domain)
|
|
231
|
+
);
|
|
109
232
|
}
|
|
233
|
+
/**
|
|
234
|
+
* @returns {string}
|
|
235
|
+
*/
|
|
110
236
|
#outboxDomain() {
|
|
111
237
|
return `revision:${this.#domain}`;
|
|
112
238
|
}
|
|
113
239
|
}
|
|
240
|
+
/**
|
|
241
|
+
* @template {JsonValue} TValue
|
|
242
|
+
* @param {unknown} value
|
|
243
|
+
* @param {RevisionedDomain} [expectedDomain]
|
|
244
|
+
* @returns {RevisionConflictRecord<TValue>}
|
|
245
|
+
*/
|
|
114
246
|
export function parseRevisionConflictRecord(value, expectedDomain) {
|
|
115
247
|
if (!isRecord(value) || value.kind !== "revision-conflict.v1")
|
|
116
248
|
throw invalid("revision conflict payload");
|
|
@@ -131,10 +263,16 @@ export function parseRevisionConflictRecord(value, expectedDomain) {
|
|
|
131
263
|
domain,
|
|
132
264
|
id: value.id,
|
|
133
265
|
idempotencyKey: value.idempotencyKey,
|
|
134
|
-
mine: value.mine,
|
|
135
|
-
theirs: value.theirs,
|
|
266
|
+
mine: /** @type {TValue} */ (value.mine),
|
|
267
|
+
theirs: /** @type {RevisionEntity<TValue>} */ (value.theirs),
|
|
136
268
|
};
|
|
137
269
|
}
|
|
270
|
+
/**
|
|
271
|
+
* @template {JsonValue} TValue
|
|
272
|
+
* @param {RevisionedDomain} domain
|
|
273
|
+
* @param {RevisionedWriteInput<TValue>} input
|
|
274
|
+
* @returns {void}
|
|
275
|
+
*/
|
|
138
276
|
function validateWriteInput(domain, input) {
|
|
139
277
|
if (!nonEmptyString(input.id) || !nonEmptyString(input.idempotencyKey))
|
|
140
278
|
throw invalid("revision write identity");
|
|
@@ -150,6 +288,10 @@ function validateWriteInput(domain, input) {
|
|
|
150
288
|
if (domain === "graph")
|
|
151
289
|
validateGraph(input.value);
|
|
152
290
|
}
|
|
291
|
+
/**
|
|
292
|
+
* @param {JsonValue} value
|
|
293
|
+
* @returns {void}
|
|
294
|
+
*/
|
|
153
295
|
function validateGraph(value) {
|
|
154
296
|
if (!isRecord(value) || !Array.isArray(value.nodes) || !Array.isArray(value.edges))
|
|
155
297
|
throw invalid("graph shape");
|
|
@@ -165,12 +307,25 @@ function validateGraph(value) {
|
|
|
165
307
|
throw invalid("graph edge");
|
|
166
308
|
}
|
|
167
309
|
}
|
|
310
|
+
/**
|
|
311
|
+
* @param {unknown} value
|
|
312
|
+
* @returns {value is RevisionEntity<JsonValue>}
|
|
313
|
+
*/
|
|
168
314
|
function isRevisionEntity(value) {
|
|
169
315
|
return isRecord(value) && nonEmptyString(value.id) && isNonNegativeInteger(value.rev) && isJsonValue(value.value);
|
|
170
316
|
}
|
|
317
|
+
/**
|
|
318
|
+
* @param {unknown} value
|
|
319
|
+
* @returns {value is RevisionedDomain}
|
|
320
|
+
*/
|
|
171
321
|
function isRevisionedDomain(value) {
|
|
172
|
-
return
|
|
322
|
+
return typeof value === "string" &&
|
|
323
|
+
REVISIONED_DOMAINS.includes(/** @type {RevisionedDomain} */ (value));
|
|
173
324
|
}
|
|
325
|
+
/**
|
|
326
|
+
* @param {unknown} value
|
|
327
|
+
* @returns {value is JsonValue}
|
|
328
|
+
*/
|
|
174
329
|
function isJsonValue(value) {
|
|
175
330
|
if (value === null || typeof value === "string" || typeof value === "boolean")
|
|
176
331
|
return true;
|
|
@@ -182,24 +337,49 @@ function isJsonValue(value) {
|
|
|
182
337
|
return false;
|
|
183
338
|
return Object.values(value).every(isJsonValue);
|
|
184
339
|
}
|
|
340
|
+
/**
|
|
341
|
+
* @param {unknown} value
|
|
342
|
+
* @returns {value is number}
|
|
343
|
+
*/
|
|
185
344
|
function isNonNegativeInteger(value) {
|
|
186
345
|
return typeof value === "number" && Number.isInteger(value) && value >= 0;
|
|
187
346
|
}
|
|
347
|
+
/**
|
|
348
|
+
* @param {unknown} value
|
|
349
|
+
* @returns {value is Record<string, unknown>}
|
|
350
|
+
*/
|
|
188
351
|
function isRecord(value) {
|
|
189
352
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
190
353
|
}
|
|
354
|
+
/**
|
|
355
|
+
* @param {unknown} value
|
|
356
|
+
* @returns {value is string}
|
|
357
|
+
*/
|
|
191
358
|
function nonEmptyString(value) {
|
|
192
359
|
return typeof value === "string" && value.trim() !== "";
|
|
193
360
|
}
|
|
361
|
+
/**
|
|
362
|
+
* @param {unknown} value
|
|
363
|
+
* @returns {string}
|
|
364
|
+
*/
|
|
194
365
|
function requiredTimestamp(value) {
|
|
195
366
|
if (!nonEmptyString(value) || !Number.isFinite(Date.parse(value)))
|
|
196
367
|
throw invalid("revision conflict timestamp");
|
|
197
368
|
return value;
|
|
198
369
|
}
|
|
370
|
+
/**
|
|
371
|
+
* @param {string} field
|
|
372
|
+
* @returns {DomainError}
|
|
373
|
+
*/
|
|
199
374
|
function invalid(field) {
|
|
200
375
|
return new DomainError({ code: "validation_failed", message: `Invalid ${field}` });
|
|
201
376
|
}
|
|
377
|
+
/**
|
|
378
|
+
* @param {RevisionedDomain} domain
|
|
379
|
+
* @param {string} id
|
|
380
|
+
* @param {string} idempotencyKey
|
|
381
|
+
* @returns {string}
|
|
382
|
+
*/
|
|
202
383
|
function conflictId(domain, id, idempotencyKey) {
|
|
203
384
|
return `${domain}:${id}:${idempotencyKey}`;
|
|
204
385
|
}
|
|
205
|
-
//# sourceMappingURL=revisioned-domain-service.js.map
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
/** @typedef {import("./abort-signal.js").AbortSignalPort} AbortSignalPort */
|
|
4
|
+
|
|
5
|
+
/** @typedef {{ abort(): void, readonly signal: AbortSignalPort }} RuntimeAbortController */
|
|
6
|
+
/** @typedef {{ readonly hostname: string, readonly origin: string, readonly password: string, readonly protocol: string, readonly username: string }} RuntimeUrl */
|
|
7
|
+
/** @typedef {new () => unknown} NoArgumentConstructor */
|
|
8
|
+
/** @typedef {new (input: string) => unknown} StringArgumentConstructor */
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @returns {RuntimeAbortController}
|
|
12
|
+
*/
|
|
13
|
+
export function createRuntimeAbortController() {
|
|
14
|
+
const Constructor = runtimeGlobal("AbortController");
|
|
15
|
+
if (!isNoArgumentConstructor(Constructor)) {
|
|
16
|
+
throw new Error("AbortController is unavailable");
|
|
17
|
+
}
|
|
18
|
+
const controller = new Constructor();
|
|
19
|
+
if (!isRuntimeAbortController(controller)) {
|
|
20
|
+
throw new TypeError("AbortController has an invalid runtime shape");
|
|
21
|
+
}
|
|
22
|
+
return controller;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @param {string} value
|
|
27
|
+
* @returns {RuntimeUrl}
|
|
28
|
+
*/
|
|
29
|
+
export function parseRuntimeUrl(value) {
|
|
30
|
+
const Constructor = runtimeGlobal("URL");
|
|
31
|
+
if (!isStringArgumentConstructor(Constructor)) {
|
|
32
|
+
throw new Error("URL is unavailable");
|
|
33
|
+
}
|
|
34
|
+
const url = new Constructor(value);
|
|
35
|
+
if (!isRuntimeUrl(url)) {
|
|
36
|
+
throw new TypeError("URL has an invalid runtime shape");
|
|
37
|
+
}
|
|
38
|
+
return url;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @param {string} value
|
|
43
|
+
* @returns {Uint8Array}
|
|
44
|
+
*/
|
|
45
|
+
export function encodeUtf8(value) {
|
|
46
|
+
const Constructor = runtimeGlobal("TextEncoder");
|
|
47
|
+
if (!isNoArgumentConstructor(Constructor)) {
|
|
48
|
+
throw new Error("TextEncoder is unavailable");
|
|
49
|
+
}
|
|
50
|
+
const encoder = new Constructor();
|
|
51
|
+
if (!isTextEncoder(encoder)) {
|
|
52
|
+
throw new TypeError("TextEncoder has an invalid runtime shape");
|
|
53
|
+
}
|
|
54
|
+
return encoder.encode(value);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* @param {Uint8Array} value
|
|
59
|
+
* @returns {string}
|
|
60
|
+
*/
|
|
61
|
+
export function decodeUtf8(value) {
|
|
62
|
+
const Constructor = runtimeGlobal("TextDecoder");
|
|
63
|
+
if (!isNoArgumentConstructor(Constructor)) {
|
|
64
|
+
throw new Error("TextDecoder is unavailable");
|
|
65
|
+
}
|
|
66
|
+
const decoder = new Constructor();
|
|
67
|
+
if (!isTextDecoder(decoder)) {
|
|
68
|
+
throw new TypeError("TextDecoder has an invalid runtime shape");
|
|
69
|
+
}
|
|
70
|
+
return decoder.decode(value);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Confine the reflection library's unsound return value here and require a
|
|
75
|
+
* runtime guard before every use.
|
|
76
|
+
*
|
|
77
|
+
* @param {string} name
|
|
78
|
+
* @returns {unknown}
|
|
79
|
+
*/
|
|
80
|
+
function runtimeGlobal(name) {
|
|
81
|
+
return /** @type {unknown} */ (Reflect.get(globalThis, name));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* @param {unknown} value
|
|
86
|
+
* @returns {value is NoArgumentConstructor}
|
|
87
|
+
*/
|
|
88
|
+
function isNoArgumentConstructor(value) {
|
|
89
|
+
return isConstructable(value);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* @param {unknown} value
|
|
94
|
+
* @returns {value is StringArgumentConstructor}
|
|
95
|
+
*/
|
|
96
|
+
function isStringArgumentConstructor(value) {
|
|
97
|
+
return isConstructable(value);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Test the ECMAScript [[Construct]] capability directly. Bound constructors
|
|
102
|
+
* deliberately have no own `prototype` property but remain constructable.
|
|
103
|
+
*
|
|
104
|
+
* @param {unknown} value
|
|
105
|
+
* @returns {boolean}
|
|
106
|
+
*/
|
|
107
|
+
function isConstructable(value) {
|
|
108
|
+
if (typeof value !== "function") return false;
|
|
109
|
+
try {
|
|
110
|
+
Reflect.construct(Object, [], value);
|
|
111
|
+
return true;
|
|
112
|
+
} catch {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* @param {unknown} value
|
|
119
|
+
* @returns {value is RuntimeAbortController}
|
|
120
|
+
*/
|
|
121
|
+
function isRuntimeAbortController(value) {
|
|
122
|
+
return isRecord(value) && typeof value.abort === "function" && isAbortSignal(value.signal);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* @param {unknown} value
|
|
127
|
+
* @returns {value is AbortSignalPort}
|
|
128
|
+
*/
|
|
129
|
+
function isAbortSignal(value) {
|
|
130
|
+
return isRecord(value) &&
|
|
131
|
+
typeof value.aborted === "boolean" &&
|
|
132
|
+
typeof value.addEventListener === "function" &&
|
|
133
|
+
typeof value.dispatchEvent === "function" &&
|
|
134
|
+
typeof value.removeEventListener === "function" &&
|
|
135
|
+
typeof value.throwIfAborted === "function" &&
|
|
136
|
+
"reason" in value;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* @param {unknown} value
|
|
141
|
+
* @returns {value is RuntimeUrl}
|
|
142
|
+
*/
|
|
143
|
+
function isRuntimeUrl(value) {
|
|
144
|
+
return isRecord(value) &&
|
|
145
|
+
typeof value.hostname === "string" &&
|
|
146
|
+
typeof value.origin === "string" &&
|
|
147
|
+
typeof value.password === "string" &&
|
|
148
|
+
typeof value.protocol === "string" &&
|
|
149
|
+
typeof value.username === "string";
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* @param {unknown} value
|
|
154
|
+
* @returns {value is { encode(input?: string): Uint8Array }}
|
|
155
|
+
*/
|
|
156
|
+
function isTextEncoder(value) {
|
|
157
|
+
return isRecord(value) && typeof value.encode === "function";
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* @param {unknown} value
|
|
162
|
+
* @returns {value is { decode(input?: Uint8Array): string }}
|
|
163
|
+
*/
|
|
164
|
+
function isTextDecoder(value) {
|
|
165
|
+
return isRecord(value) && typeof value.decode === "function";
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* @param {unknown} value
|
|
170
|
+
* @returns {value is Record<string, unknown>}
|
|
171
|
+
*/
|
|
172
|
+
function isRecord(value) {
|
|
173
|
+
return typeof value === "object" && value !== null;
|
|
174
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @typedef {import("@econ-v1/ports").DeviceDelegation} DeviceDelegation
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* @typedef {import("@econ-v1/ports").DeviceIdentity} DeviceIdentity
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* @typedef {import("@econ-v1/ports").IdentityDiagnostics} IdentityDiagnostics
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* @typedef {import("@econ-v1/ports").IdentityPort} IdentityPort
|
|
14
|
+
*/
|
|
15
|
+
import { normalizeDelegation } from "../models/delegation.js";
|
|
16
|
+
export class SessionService {
|
|
17
|
+
/** @readonly @type {IdentityPort} */
|
|
18
|
+
identity;
|
|
19
|
+
/**
|
|
20
|
+
* @param {IdentityPort} identity
|
|
21
|
+
*/
|
|
22
|
+
constructor(identity) {
|
|
23
|
+
this.identity = identity;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* @returns {Promise<DeviceIdentity>}
|
|
27
|
+
*/
|
|
28
|
+
async initialize() {
|
|
29
|
+
return await this.identity.generate();
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* @param {DeviceDelegation} delegation
|
|
33
|
+
* @returns {Promise<void>}
|
|
34
|
+
*/
|
|
35
|
+
async acceptDelegation(delegation) {
|
|
36
|
+
await this.identity.storeDelegation(normalizeDelegation(delegation));
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* @returns {Promise<void>}
|
|
40
|
+
*/
|
|
41
|
+
async revoke() {
|
|
42
|
+
await this.identity.clear();
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* @returns {Promise<IdentityDiagnostics>}
|
|
46
|
+
*/
|
|
47
|
+
async diagnostics() {
|
|
48
|
+
return await this.identity.diagnostics();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -1,19 +1,87 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @template TPayload
|
|
5
|
+
* @typedef {import("../../models/sync.js").AuthoritativeCommand<TPayload>} AuthoritativeCommand
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* @template TValue
|
|
9
|
+
* @typedef {import("../../models/sync.js").AuthoritativeSnapshot<TValue>} AuthoritativeSnapshot
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* @typedef {import("../../models/sync.js").SyncApplyResult} SyncApplyResult
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* @template TPayload
|
|
16
|
+
* @typedef {import("../../models/sync.js").SyncDelta<TPayload>} SyncDelta
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* @typedef {import("../../models/sync.js").SyncDeltaContext} SyncDeltaContext
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* @template TPayload
|
|
23
|
+
* @typedef {import("../../models/sync.js").SyncStrategy<TPayload>} SyncStrategy
|
|
24
|
+
*/
|
|
25
|
+
/**
|
|
26
|
+
* @template TValue
|
|
27
|
+
* @template TCommand
|
|
28
|
+
* @template TResult
|
|
29
|
+
* @typedef {{ readonly apply: (snapshot: AuthoritativeSnapshot<TValue>, delta: SyncDelta<TValue>) => Promise<void>; readonly execute: (command: AuthoritativeCommand<TCommand>) => Promise<TResult>; readonly staleAfterMs: number; }} AuthoritativeMirrorOptions
|
|
30
|
+
*/
|
|
31
|
+
/**
|
|
32
|
+
* @typedef {{ readonly now: string; readonly revalidate?: () => Promise<boolean>; }} AuthoritativeCommandOptions
|
|
33
|
+
*/
|
|
34
|
+
/**
|
|
35
|
+
* @extends {Error}
|
|
36
|
+
*/
|
|
1
37
|
export class AuthoritativeRevalidationRequiredError extends Error {
|
|
38
|
+
/**
|
|
39
|
+
* @readonly
|
|
40
|
+
* @type {string}
|
|
41
|
+
*/
|
|
2
42
|
idempotencyKey;
|
|
43
|
+
/**
|
|
44
|
+
* @param {string} idempotencyKey
|
|
45
|
+
*/
|
|
3
46
|
constructor(idempotencyKey) {
|
|
4
47
|
super(`Authoritative command ${idempotencyKey} requires fresh state revalidation`);
|
|
5
48
|
this.name = "AuthoritativeRevalidationRequiredError";
|
|
6
49
|
this.idempotencyKey = idempotencyKey;
|
|
7
50
|
}
|
|
8
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* @template TValue
|
|
54
|
+
* @template TCommand
|
|
55
|
+
* @template TResult
|
|
56
|
+
* @implements {SyncStrategy<TValue>}
|
|
57
|
+
*/
|
|
9
58
|
export class AuthoritativeMirror {
|
|
59
|
+
/** @readonly @type {"authoritative-mirror"} */
|
|
10
60
|
kind = "authoritative-mirror";
|
|
61
|
+
/**
|
|
62
|
+
* @readonly
|
|
63
|
+
* @type {AuthoritativeMirrorOptions<TValue, TCommand, TResult>["apply"]}
|
|
64
|
+
*/
|
|
11
65
|
#apply;
|
|
66
|
+
/**
|
|
67
|
+
* @readonly
|
|
68
|
+
* @type {AuthoritativeMirrorOptions<TValue, TCommand, TResult>["execute"]}
|
|
69
|
+
*/
|
|
12
70
|
#execute;
|
|
71
|
+
/**
|
|
72
|
+
* @readonly
|
|
73
|
+
* @type {number}
|
|
74
|
+
*/
|
|
13
75
|
#staleAfterMs;
|
|
14
76
|
#applied = new Set();
|
|
15
77
|
#commands = new Map();
|
|
78
|
+
/**
|
|
79
|
+
* @type {AuthoritativeSnapshot<TValue> | undefined}
|
|
80
|
+
*/
|
|
16
81
|
#snapshot;
|
|
82
|
+
/**
|
|
83
|
+
* @param {AuthoritativeMirrorOptions<TValue, TCommand, TResult>} options
|
|
84
|
+
*/
|
|
17
85
|
constructor(options) {
|
|
18
86
|
if (!Number.isInteger(options.staleAfterMs) || options.staleAfterMs <= 0) {
|
|
19
87
|
throw new RangeError("Authoritative mirror staleAfterMs must be a positive integer");
|
|
@@ -22,6 +90,11 @@ export class AuthoritativeMirror {
|
|
|
22
90
|
this.#execute = options.execute;
|
|
23
91
|
this.#staleAfterMs = options.staleAfterMs;
|
|
24
92
|
}
|
|
93
|
+
/**
|
|
94
|
+
* @param {SyncDelta<TValue>} delta
|
|
95
|
+
* @param {SyncDeltaContext} context
|
|
96
|
+
* @returns {Promise<SyncApplyResult>}
|
|
97
|
+
*/
|
|
25
98
|
async applyDelta(delta, context) {
|
|
26
99
|
if (this.#applied.has(delta.id))
|
|
27
100
|
return { applied: false };
|
|
@@ -36,12 +109,24 @@ export class AuthoritativeMirror {
|
|
|
36
109
|
this.#applied.add(delta.id);
|
|
37
110
|
return { applied: true };
|
|
38
111
|
}
|
|
112
|
+
/**
|
|
113
|
+
* @returns {AuthoritativeSnapshot<TValue> | undefined}
|
|
114
|
+
*/
|
|
39
115
|
snapshot() {
|
|
40
116
|
return this.#snapshot;
|
|
41
117
|
}
|
|
118
|
+
/**
|
|
119
|
+
* @param {string} now
|
|
120
|
+
* @returns {boolean}
|
|
121
|
+
*/
|
|
42
122
|
isStale(now) {
|
|
43
123
|
return this.#snapshot === undefined || parseDate(now, "current time") >= Date.parse(this.#snapshot.staleAt);
|
|
44
124
|
}
|
|
125
|
+
/**
|
|
126
|
+
* @param {AuthoritativeCommand<TCommand>} command
|
|
127
|
+
* @param {AuthoritativeCommandOptions} options
|
|
128
|
+
* @returns {Promise<TResult>}
|
|
129
|
+
*/
|
|
45
130
|
async runCommand(command, options) {
|
|
46
131
|
const existing = this.#commands.get(command.idempotencyKey);
|
|
47
132
|
if (existing !== undefined)
|
|
@@ -62,10 +147,14 @@ export class AuthoritativeMirror {
|
|
|
62
147
|
}
|
|
63
148
|
}
|
|
64
149
|
}
|
|
150
|
+
/**
|
|
151
|
+
* @param {string} value
|
|
152
|
+
* @param {string} label
|
|
153
|
+
* @returns {number}
|
|
154
|
+
*/
|
|
65
155
|
function parseDate(value, label) {
|
|
66
156
|
const parsed = Date.parse(value);
|
|
67
157
|
if (!Number.isFinite(parsed))
|
|
68
158
|
throw new RangeError(`Invalid ${label}`);
|
|
69
159
|
return parsed;
|
|
70
160
|
}
|
|
71
|
-
//# sourceMappingURL=authoritative-mirror.js.map
|