@meshline/sdk 0.0.0-stage → 0.1.0-alpha.1
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/LICENSE +21 -0
- package/README.md +43 -2
- package/dist/channels/repository.d.ts +73 -0
- package/dist/channels/repository.d.ts.map +1 -0
- package/dist/channels/repository.js +367 -0
- package/dist/channels/repository.js.map +1 -0
- package/dist/client/account-operations.d.ts +41 -0
- package/dist/client/account-operations.d.ts.map +1 -0
- package/dist/client/account-operations.js +304 -0
- package/dist/client/account-operations.js.map +1 -0
- package/dist/client/conversations.d.ts +36 -0
- package/dist/client/conversations.d.ts.map +1 -0
- package/dist/client/conversations.js +144 -0
- package/dist/client/conversations.js.map +1 -0
- package/dist/client.d.ts +53 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +162 -0
- package/dist/client.js.map +1 -0
- package/dist/components/account.d.ts +38 -0
- package/dist/components/account.d.ts.map +1 -0
- package/dist/components/account.js +243 -0
- package/dist/components/account.js.map +1 -0
- package/dist/components/channel.d.ts +78 -0
- package/dist/components/channel.d.ts.map +1 -0
- package/dist/components/channel.js +475 -0
- package/dist/components/channel.js.map +1 -0
- package/dist/components/component.d.ts +49 -0
- package/dist/components/component.d.ts.map +1 -0
- package/dist/components/component.js +194 -0
- package/dist/components/component.js.map +1 -0
- package/dist/components/device.d.ts +60 -0
- package/dist/components/device.d.ts.map +1 -0
- package/dist/components/device.js +648 -0
- package/dist/components/device.js.map +1 -0
- package/dist/components/group.d.ts +141 -0
- package/dist/components/group.d.ts.map +1 -0
- package/dist/components/group.js +933 -0
- package/dist/components/group.js.map +1 -0
- package/dist/components/message.d.ts +63 -0
- package/dist/components/message.d.ts.map +1 -0
- package/dist/components/message.js +439 -0
- package/dist/components/message.js.map +1 -0
- package/dist/components/profile.d.ts +37 -0
- package/dist/components/profile.d.ts.map +1 -0
- package/dist/components/profile.js +232 -0
- package/dist/components/profile.js.map +1 -0
- package/dist/crypto/groups.d.ts +56 -0
- package/dist/crypto/groups.d.ts.map +1 -0
- package/dist/crypto/groups.js +163 -0
- package/dist/crypto/groups.js.map +1 -0
- package/dist/crypto/messages.d.ts +52 -0
- package/dist/crypto/messages.d.ts.map +1 -0
- package/dist/crypto/messages.js +180 -0
- package/dist/crypto/messages.js.map +1 -0
- package/dist/crypto/primitives.d.ts +30 -0
- package/dist/crypto/primitives.d.ts.map +1 -0
- package/dist/crypto/primitives.js +102 -0
- package/dist/crypto/primitives.js.map +1 -0
- package/dist/errors.d.ts +11 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +14 -0
- package/dist/errors.js.map +1 -0
- package/dist/groups/account-sync.d.ts +28 -0
- package/dist/groups/account-sync.d.ts.map +1 -0
- package/dist/groups/account-sync.js +235 -0
- package/dist/groups/account-sync.js.map +1 -0
- package/dist/groups/messages.d.ts +39 -0
- package/dist/groups/messages.d.ts.map +1 -0
- package/dist/groups/messages.js +141 -0
- package/dist/groups/messages.js.map +1 -0
- package/dist/groups/operations.d.ts +57 -0
- package/dist/groups/operations.d.ts.map +1 -0
- package/dist/groups/operations.js +225 -0
- package/dist/groups/operations.js.map +1 -0
- package/dist/groups/repository.d.ts +56 -0
- package/dist/groups/repository.d.ts.map +1 -0
- package/dist/groups/repository.js +254 -0
- package/dist/groups/repository.js.map +1 -0
- package/dist/groups/secrets.d.ts +67 -0
- package/dist/groups/secrets.d.ts.map +1 -0
- package/dist/groups/secrets.js +452 -0
- package/dist/groups/secrets.js.map +1 -0
- package/dist/groups/state.d.ts +39 -0
- package/dist/groups/state.d.ts.map +1 -0
- package/dist/groups/state.js +201 -0
- package/dist/groups/state.js.map +1 -0
- package/dist/identity/identifiers.d.ts +9 -0
- package/dist/identity/identifiers.d.ts.map +1 -0
- package/dist/identity/identifiers.js +37 -0
- package/dist/identity/identifiers.js.map +1 -0
- package/dist/identity/neo.d.ts +11 -0
- package/dist/identity/neo.d.ts.map +1 -0
- package/dist/identity/neo.js +77 -0
- package/dist/identity/neo.js.map +1 -0
- package/dist/index.d.ts +53 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +46 -0
- package/dist/index.js.map +1 -0
- package/dist/interactions.d.ts +31 -0
- package/dist/interactions.d.ts.map +1 -0
- package/dist/interactions.js +2 -0
- package/dist/interactions.js.map +1 -0
- package/dist/messages/account-sender.d.ts +9 -0
- package/dist/messages/account-sender.d.ts.map +1 -0
- package/dist/messages/account-sender.js +46 -0
- package/dist/messages/account-sender.js.map +1 -0
- package/dist/messages/contacts.d.ts +103 -0
- package/dist/messages/contacts.d.ts.map +1 -0
- package/dist/messages/contacts.js +525 -0
- package/dist/messages/contacts.js.map +1 -0
- package/dist/messages/outbox.d.ts +66 -0
- package/dist/messages/outbox.d.ts.map +1 -0
- package/dist/messages/outbox.js +167 -0
- package/dist/messages/outbox.js.map +1 -0
- package/dist/messages/receiver.d.ts +35 -0
- package/dist/messages/receiver.d.ts.map +1 -0
- package/dist/messages/receiver.js +113 -0
- package/dist/messages/receiver.js.map +1 -0
- package/dist/messages/repository.d.ts +89 -0
- package/dist/messages/repository.d.ts.map +1 -0
- package/dist/messages/repository.js +295 -0
- package/dist/messages/repository.js.map +1 -0
- package/dist/models/authentication.d.ts +37 -0
- package/dist/models/authentication.d.ts.map +1 -0
- package/dist/models/authentication.js +53 -0
- package/dist/models/authentication.js.map +1 -0
- package/dist/models/channels.d.ts +141 -0
- package/dist/models/channels.d.ts.map +1 -0
- package/dist/models/channels.js +258 -0
- package/dist/models/channels.js.map +1 -0
- package/dist/models/contacts.d.ts +68 -0
- package/dist/models/contacts.d.ts.map +1 -0
- package/dist/models/contacts.js +186 -0
- package/dist/models/contacts.js.map +1 -0
- package/dist/models/content.d.ts +37 -0
- package/dist/models/content.d.ts.map +1 -0
- package/dist/models/content.js +137 -0
- package/dist/models/content.js.map +1 -0
- package/dist/models/device-state.d.ts +8 -0
- package/dist/models/device-state.d.ts.map +1 -0
- package/dist/models/device-state.js +22 -0
- package/dist/models/device-state.js.map +1 -0
- package/dist/models/group-admission.d.ts +111 -0
- package/dist/models/group-admission.d.ts.map +1 -0
- package/dist/models/group-admission.js +153 -0
- package/dist/models/group-admission.js.map +1 -0
- package/dist/models/group-keys.d.ts +30 -0
- package/dist/models/group-keys.d.ts.map +1 -0
- package/dist/models/group-keys.js +52 -0
- package/dist/models/group-keys.js.map +1 -0
- package/dist/models/group-management.d.ts +190 -0
- package/dist/models/group-management.d.ts.map +1 -0
- package/dist/models/group-management.js +263 -0
- package/dist/models/group-management.js.map +1 -0
- package/dist/models/groups.d.ts +81 -0
- package/dist/models/groups.d.ts.map +1 -0
- package/dist/models/groups.js +127 -0
- package/dist/models/groups.js.map +1 -0
- package/dist/models/identity.d.ts +47 -0
- package/dist/models/identity.d.ts.map +1 -0
- package/dist/models/identity.js +136 -0
- package/dist/models/identity.js.map +1 -0
- package/dist/models/messages.d.ts +104 -0
- package/dist/models/messages.d.ts.map +1 -0
- package/dist/models/messages.js +168 -0
- package/dist/models/messages.js.map +1 -0
- package/dist/models/profile.d.ts +24 -0
- package/dist/models/profile.d.ts.map +1 -0
- package/dist/models/profile.js +47 -0
- package/dist/models/profile.js.map +1 -0
- package/dist/models/relay.d.ts +39 -0
- package/dist/models/relay.d.ts.map +1 -0
- package/dist/models/relay.js +160 -0
- package/dist/models/relay.js.map +1 -0
- package/dist/protocol/codec.d.ts +36 -0
- package/dist/protocol/codec.d.ts.map +1 -0
- package/dist/protocol/codec.js +144 -0
- package/dist/protocol/codec.js.map +1 -0
- package/dist/protocol/context.d.ts +14 -0
- package/dist/protocol/context.d.ts.map +1 -0
- package/dist/protocol/context.js +43 -0
- package/dist/protocol/context.js.map +1 -0
- package/dist/protocol/encoding.d.ts +15 -0
- package/dist/protocol/encoding.d.ts.map +1 -0
- package/dist/protocol/encoding.js +133 -0
- package/dist/protocol/encoding.js.map +1 -0
- package/dist/protocol/json.d.ts +16 -0
- package/dist/protocol/json.d.ts.map +1 -0
- package/dist/protocol/json.js +172 -0
- package/dist/protocol/json.js.map +1 -0
- package/dist/protocol/validation.d.ts +4 -0
- package/dist/protocol/validation.d.ts.map +1 -0
- package/dist/protocol/validation.js +18 -0
- package/dist/protocol/validation.js.map +1 -0
- package/dist/runtime/abort.d.ts +6 -0
- package/dist/runtime/abort.d.ts.map +1 -0
- package/dist/runtime/abort.js +32 -0
- package/dist/runtime/abort.js.map +1 -0
- package/dist/runtime/async-gate.d.ts +6 -0
- package/dist/runtime/async-gate.d.ts.map +1 -0
- package/dist/runtime/async-gate.js +15 -0
- package/dist/runtime/async-gate.js.map +1 -0
- package/dist/runtime/async-pulse.d.ts +7 -0
- package/dist/runtime/async-pulse.d.ts.map +1 -0
- package/dist/runtime/async-pulse.js +23 -0
- package/dist/runtime/async-pulse.js.map +1 -0
- package/dist/runtime/clock.d.ts +16 -0
- package/dist/runtime/clock.d.ts.map +1 -0
- package/dist/runtime/clock.js +62 -0
- package/dist/runtime/clock.js.map +1 -0
- package/dist/runtime/events.d.ts +11 -0
- package/dist/runtime/events.d.ts.map +1 -0
- package/dist/runtime/events.js +45 -0
- package/dist/runtime/events.js.map +1 -0
- package/dist/runtime/url.d.ts +3 -0
- package/dist/runtime/url.d.ts.map +1 -0
- package/dist/runtime/url.js +23 -0
- package/dist/runtime/url.js.map +1 -0
- package/dist/storage/signed-request.d.ts +14 -0
- package/dist/storage/signed-request.d.ts.map +1 -0
- package/dist/storage/signed-request.js +26 -0
- package/dist/storage/signed-request.js.map +1 -0
- package/dist/storage/store.d.ts +58 -0
- package/dist/storage/store.d.ts.map +1 -0
- package/dist/storage/store.js +52 -0
- package/dist/storage/store.js.map +1 -0
- package/dist/storage/transaction.d.ts +7 -0
- package/dist/storage/transaction.d.ts.map +1 -0
- package/dist/storage/transaction.js +22 -0
- package/dist/storage/transaction.js.map +1 -0
- package/dist/transport/client.d.ts +61 -0
- package/dist/transport/client.d.ts.map +1 -0
- package/dist/transport/client.js +392 -0
- package/dist/transport/client.js.map +1 -0
- package/dist/transport/discovery.d.ts +19 -0
- package/dist/transport/discovery.d.ts.map +1 -0
- package/dist/transport/discovery.js +75 -0
- package/dist/transport/discovery.js.map +1 -0
- package/dist/transport/endpoint.d.ts +6 -0
- package/dist/transport/endpoint.d.ts.map +1 -0
- package/dist/transport/endpoint.js +35 -0
- package/dist/transport/endpoint.js.map +1 -0
- package/dist/transport/http.d.ts +52 -0
- package/dist/transport/http.d.ts.map +1 -0
- package/dist/transport/http.js +128 -0
- package/dist/transport/http.js.map +1 -0
- package/dist/transport/pool.d.ts +31 -0
- package/dist/transport/pool.d.ts.map +1 -0
- package/dist/transport/pool.js +137 -0
- package/dist/transport/pool.js.map +1 -0
- package/dist/transport/relay-error.d.ts +23 -0
- package/dist/transport/relay-error.d.ts.map +1 -0
- package/dist/transport/relay-error.js +47 -0
- package/dist/transport/relay-error.js.map +1 -0
- package/dist/transport/rpc.d.ts +31 -0
- package/dist/transport/rpc.d.ts.map +1 -0
- package/dist/transport/rpc.js +56 -0
- package/dist/transport/rpc.js.map +1 -0
- package/dist/transport/session.d.ts +49 -0
- package/dist/transport/session.d.ts.map +1 -0
- package/dist/transport/session.js +169 -0
- package/dist/transport/session.js.map +1 -0
- package/dist/transport/socket.d.ts +55 -0
- package/dist/transport/socket.d.ts.map +1 -0
- package/dist/transport/socket.js +266 -0
- package/dist/transport/socket.js.map +1 -0
- package/dist/transport/subscription.d.ts +36 -0
- package/dist/transport/subscription.d.ts.map +1 -0
- package/dist/transport/subscription.js +169 -0
- package/dist/transport/subscription.js.map +1 -0
- package/package.json +35 -4
- package/src/channels/repository.ts +244 -0
- package/src/client/account-operations.ts +183 -0
- package/src/client/conversations.ts +95 -0
- package/src/client.ts +108 -0
- package/src/components/account.ts +206 -0
- package/src/components/channel.ts +253 -0
- package/src/components/component.ts +151 -0
- package/src/components/device.ts +455 -0
- package/src/components/group.ts +546 -0
- package/src/components/message.ts +253 -0
- package/src/components/profile.ts +179 -0
- package/src/crypto/groups.ts +107 -0
- package/src/crypto/messages.ts +129 -0
- package/src/crypto/primitives.ts +103 -0
- package/src/errors.ts +13 -0
- package/src/groups/account-sync.ts +144 -0
- package/src/groups/messages.ts +89 -0
- package/src/groups/operations.ts +145 -0
- package/src/groups/repository.ts +161 -0
- package/src/groups/secrets.ts +256 -0
- package/src/groups/state.ts +138 -0
- package/src/identity/identifiers.ts +41 -0
- package/src/identity/neo.ts +72 -0
- package/src/index.ts +52 -0
- package/src/interactions.ts +35 -0
- package/src/messages/account-sender.ts +33 -0
- package/src/messages/contacts.ts +379 -0
- package/src/messages/outbox.ts +152 -0
- package/src/messages/receiver.ts +86 -0
- package/src/messages/repository.ts +230 -0
- package/src/models/authentication.ts +63 -0
- package/src/models/channels.ts +155 -0
- package/src/models/contacts.ts +141 -0
- package/src/models/content.ts +105 -0
- package/src/models/device-state.ts +18 -0
- package/src/models/group-admission.ts +110 -0
- package/src/models/group-keys.ts +34 -0
- package/src/models/group-management.ts +182 -0
- package/src/models/groups.ts +100 -0
- package/src/models/identity.ts +164 -0
- package/src/models/messages.ts +139 -0
- package/src/models/profile.ts +46 -0
- package/src/models/relay.ts +151 -0
- package/src/protocol/codec.ts +144 -0
- package/src/protocol/context.ts +39 -0
- package/src/protocol/encoding.ts +106 -0
- package/src/protocol/json.ts +140 -0
- package/src/protocol/validation.ts +13 -0
- package/src/runtime/abort.ts +27 -0
- package/src/runtime/async-gate.ts +16 -0
- package/src/runtime/async-pulse.ts +20 -0
- package/src/runtime/clock.ts +63 -0
- package/src/runtime/events.ts +31 -0
- package/src/runtime/url.ts +17 -0
- package/src/storage/signed-request.ts +26 -0
- package/src/storage/store.ts +90 -0
- package/src/storage/transaction.ts +17 -0
- package/src/transport/client.ts +328 -0
- package/src/transport/discovery.ts +64 -0
- package/src/transport/endpoint.ts +32 -0
- package/src/transport/http.ts +129 -0
- package/src/transport/pool.ts +115 -0
- package/src/transport/relay-error.ts +46 -0
- package/src/transport/rpc.ts +56 -0
- package/src/transport/session.ts +154 -0
- package/src/transport/socket.ts +248 -0
- package/src/transport/subscription.ts +117 -0
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { createAbortController } from '../runtime/abort.js';
|
|
2
|
+
import { ProtocolError } from '../errors.js';
|
|
3
|
+
import { canonicalJson, parseJson, requireObject } from '../protocol/json.js';
|
|
4
|
+
import { AsyncPulse } from '../runtime/async-pulse.js';
|
|
5
|
+
import { abortScope, throwIfAborted } from '../runtime/clock.js';
|
|
6
|
+
import { RelayError } from './relay-error.js';
|
|
7
|
+
function transient(error) {
|
|
8
|
+
if (error instanceof RelayError)
|
|
9
|
+
return ['unauthorized', 'temporarily_unavailable', 'bad_gateway', 'rate_limited'].includes(error.code);
|
|
10
|
+
if (error instanceof ProtocolError)
|
|
11
|
+
return ['socket_closed', 'socket_error', 'notification_overflow'].includes(error.code);
|
|
12
|
+
return error instanceof TypeError || error instanceof Error && (['AbortError', 'TimeoutError'].includes(error.name) || 'code' in error);
|
|
13
|
+
}
|
|
14
|
+
/** One full-replacement stream for one method and relay. Created through RelayClient. */
|
|
15
|
+
export class RelaySubscription {
|
|
16
|
+
method;
|
|
17
|
+
options;
|
|
18
|
+
transport;
|
|
19
|
+
#wake = new AsyncPulse();
|
|
20
|
+
#stop = createAbortController();
|
|
21
|
+
#pending = createAbortController();
|
|
22
|
+
#scope;
|
|
23
|
+
#pendingScope;
|
|
24
|
+
#empty;
|
|
25
|
+
#detach;
|
|
26
|
+
#desired;
|
|
27
|
+
#worker;
|
|
28
|
+
#connection;
|
|
29
|
+
#applied;
|
|
30
|
+
#appliedGeneration;
|
|
31
|
+
#lastError;
|
|
32
|
+
#disposal;
|
|
33
|
+
constructor(method, empty, options, transport) {
|
|
34
|
+
this.method = method;
|
|
35
|
+
this.options = options;
|
|
36
|
+
this.transport = transport;
|
|
37
|
+
this.#empty = canonicalJson(requireObject(empty));
|
|
38
|
+
this.#scope = abortScope([options.signal, transport.lifetime, this.#stop.signal]);
|
|
39
|
+
this.#pendingScope = abortScope([transport.lifetime, this.#pending.signal]);
|
|
40
|
+
this.#detach = transport.onConnected(() => this.#wake.pulse());
|
|
41
|
+
}
|
|
42
|
+
get lastError() { return this.#lastError; }
|
|
43
|
+
/** Snapshots and coalesces updates, including empty sets; it does not wait for remote acknowledgement. */
|
|
44
|
+
update(request) {
|
|
45
|
+
throwIfAborted(this.#scope.signal);
|
|
46
|
+
this.#desired = canonicalJson(requireObject(request));
|
|
47
|
+
this.#wake.pulse();
|
|
48
|
+
this.#worker ??= this.#run();
|
|
49
|
+
}
|
|
50
|
+
#report(error) {
|
|
51
|
+
this.#lastError = error;
|
|
52
|
+
try {
|
|
53
|
+
Promise.resolve(this.options.onError?.(error)).catch(observer => { this.#lastError = new AggregateError([error, observer], 'Subscription and error observer failed.'); });
|
|
54
|
+
}
|
|
55
|
+
catch (observer) {
|
|
56
|
+
this.#lastError = new AggregateError([error, observer], 'Subscription and error observer failed.');
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
#subscribed() {
|
|
60
|
+
try {
|
|
61
|
+
Promise.resolve(this.options.onSubscribed?.()).catch(error => this.#report(error));
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
this.#report(error);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
async #send(connection, json) {
|
|
68
|
+
throwIfAborted(this.#pendingScope.signal);
|
|
69
|
+
try {
|
|
70
|
+
const result = await connection.request(this.method, requireObject(parseJson(json)), this.#pendingScope.signal);
|
|
71
|
+
if (result !== null)
|
|
72
|
+
throw new ProtocolError('invalid_response', 'Subscription replacement must acknowledge with JSON null.');
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
if (!(error instanceof RelayError))
|
|
76
|
+
connection.retire(new ProtocolError('socket_closed', 'Subscription outcome is unknown; reconnect before replacing the set.', { cause: error }));
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
async #retry() {
|
|
81
|
+
const timer = createAbortController();
|
|
82
|
+
const scope = abortScope([this.#scope.signal, timer.signal]);
|
|
83
|
+
const delay = this.transport.clock.delay(5000, scope.signal).then(() => this.#wake.pulse(), error => { if (!scope.signal.aborted)
|
|
84
|
+
this.#report(error); });
|
|
85
|
+
try {
|
|
86
|
+
await this.#wake.wait(this.#scope.signal);
|
|
87
|
+
this.#wake.pulse();
|
|
88
|
+
}
|
|
89
|
+
finally {
|
|
90
|
+
timer.abort();
|
|
91
|
+
await delay;
|
|
92
|
+
scope.dispose();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
async #run() {
|
|
96
|
+
try {
|
|
97
|
+
for (;;) {
|
|
98
|
+
await this.#wake.wait(this.#scope.signal);
|
|
99
|
+
try {
|
|
100
|
+
const connection = await this.transport.ready(this.#scope.signal);
|
|
101
|
+
// Take the latest set only after this relay becomes ready.
|
|
102
|
+
const request = this.#desired;
|
|
103
|
+
if (request === undefined)
|
|
104
|
+
continue;
|
|
105
|
+
if (this.#applied === request && this.#appliedGeneration === connection.generation)
|
|
106
|
+
continue;
|
|
107
|
+
throwIfAborted(this.#scope.signal);
|
|
108
|
+
this.#applied = undefined;
|
|
109
|
+
this.#connection = connection;
|
|
110
|
+
await this.#send(connection, request);
|
|
111
|
+
throwIfAborted(this.#scope.signal);
|
|
112
|
+
this.#applied = request;
|
|
113
|
+
this.#appliedGeneration = connection.generation;
|
|
114
|
+
if (request !== this.#empty)
|
|
115
|
+
this.#subscribed();
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
if (this.#scope.signal.aborted) {
|
|
119
|
+
if (this.#pendingScope.signal.aborted && !this.transport.lifetime.aborted)
|
|
120
|
+
this.#report(error);
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
this.#report(error);
|
|
124
|
+
if (transient(error))
|
|
125
|
+
await this.#retry();
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
if (!this.#scope.signal.aborted)
|
|
131
|
+
this.#report(error);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
/** Drains the in-flight replacement and clears this exact connection within one shared five-second budget. */
|
|
135
|
+
dispose() {
|
|
136
|
+
return this.#disposal ??= (async () => {
|
|
137
|
+
this.#stop.abort(new DOMException('Subscription stopped.', 'AbortError'));
|
|
138
|
+
this.#detach();
|
|
139
|
+
const timer = createAbortController();
|
|
140
|
+
const budget = this.transport.clock.delay(5000, timer.signal).then(() => this.#pending.abort(new DOMException('Subscription cleanup timed out.', 'TimeoutError')), error => {
|
|
141
|
+
if (!timer.signal.aborted) {
|
|
142
|
+
this.#pending.abort(error);
|
|
143
|
+
this.#report(error);
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
try {
|
|
147
|
+
await this.#worker;
|
|
148
|
+
const connection = this.#connection;
|
|
149
|
+
if (connection && !connection.failure) {
|
|
150
|
+
try {
|
|
151
|
+
await this.#send(connection, this.#empty);
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
connection.retire(new ProtocolError('socket_closed', 'Subscriptions could not be cleared; retire the old connection.', { cause: error }));
|
|
155
|
+
this.#report(error);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
finally {
|
|
160
|
+
timer.abort();
|
|
161
|
+
await budget;
|
|
162
|
+
this.#scope.dispose();
|
|
163
|
+
this.#pendingScope.dispose();
|
|
164
|
+
this.transport.closed();
|
|
165
|
+
}
|
|
166
|
+
})();
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
//# sourceMappingURL=subscription.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"subscription.js","sourceRoot":"","sources":["../../src/transport/subscription.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,37 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@meshline/sdk",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0-alpha.1",
|
|
4
|
+
"description": "Meshline protocol and client SDK for TypeScript",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"sideEffects": false,
|
|
8
|
+
"files": [
|
|
9
|
+
"dist",
|
|
10
|
+
"src",
|
|
11
|
+
"README.md",
|
|
12
|
+
"LICENSE"
|
|
13
|
+
],
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"import": "./dist/index.js"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "https://github.com/meshline-network/sdk.git",
|
|
23
|
+
"directory": "typescript/packages/sdk"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@multiformats/multiaddr": "13.0.3",
|
|
27
|
+
"@noble/ciphers": "2.4.0",
|
|
28
|
+
"@noble/curves": "2.4.0",
|
|
29
|
+
"@noble/hashes": "2.4.0",
|
|
30
|
+
"multiformats": "14.0.5",
|
|
31
|
+
"tr46": "6.0.0"
|
|
32
|
+
},
|
|
33
|
+
"publishConfig": {
|
|
34
|
+
"access": "public",
|
|
35
|
+
"registry": "https://registry.npmjs.org"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { ProtocolError, StateConflictError } from '../errors.js';
|
|
2
|
+
import {
|
|
3
|
+
applyChannelPostEdit, channelDescriptorCodec, channelEventCodec, channelPayloadCodec, channelPostCodec, channelReadPageCodec, channelResolveResultCodec,
|
|
4
|
+
validateChannelPayload, validateChannelReadPage, validateChannelReadQuery, validateChannelRef, validateChannelPostRef, verifyChannelDescriptor, verifyChannelEvent,
|
|
5
|
+
type ChannelDescriptor, type ChannelEvent, type ChannelPayload, type ChannelPost, type ChannelPostRef, type ChannelReadPage, type ChannelReadQuery, type ChannelRef, type ChannelResolveResult,
|
|
6
|
+
} from '../models/channels.js';
|
|
7
|
+
import type { DeviceCertificate } from '../models/identity.js';
|
|
8
|
+
import type { ContentReference, MessageBody } from '../models/content.js';
|
|
9
|
+
import type { NetworkContext } from '../protocol/context.js';
|
|
10
|
+
import { canonicalJson, requireObject, requireSafeInteger, type JsonObject } from '../protocol/json.js';
|
|
11
|
+
import type { MeshlineStore, QueryReader, RecordKey, RecordQuery, StoredRecord, StoreMutation, StoreSnapshot } from '../storage/store.js';
|
|
12
|
+
import { updateStore } from '../storage/transaction.js';
|
|
13
|
+
import { snapshotReader } from '../messages/repository.js';
|
|
14
|
+
|
|
15
|
+
export interface ChannelInfo { readonly ref: ChannelRef; readonly descriptor?: ChannelDescriptor; readonly isFollowed: boolean }
|
|
16
|
+
export interface ChannelPostInfo { readonly ref: ChannelPostRef; readonly messageId: string; readonly author: string; readonly acceptedAt: number; readonly body?: MessageBody; readonly attachments?: readonly ContentReference[] }
|
|
17
|
+
export interface ChannelPostChange { readonly ref: ChannelPostRef; readonly kind: 'added' | 'edited' | 'deleted'; readonly info?: ChannelPostInfo }
|
|
18
|
+
export interface ChannelChanges { readonly channel?: ChannelInfo; readonly posts: readonly ChannelPostChange[] }
|
|
19
|
+
export interface ChannelRecord { readonly channel: ChannelRef; readonly descriptor?: ChannelDescriptor; readonly syncSequence: number; readonly isFollowed: boolean }
|
|
20
|
+
interface PostRecord { sequence: number; appliedThrough: number; isDeleted: boolean; messageId?: string; author?: string; acceptedAt?: number; original?: ChannelPost; post?: ChannelPost }
|
|
21
|
+
export type ChannelOperationMethod = 'channel.create' | 'channel.update' | 'channel.close' | 'channel.post' | 'channel.post.edit' | 'channel.post.delete';
|
|
22
|
+
export interface ChannelOperation { readonly channel: ChannelRef; readonly method: ChannelOperationMethod; readonly payload: ChannelPayload; readonly acceptedSequence?: number }
|
|
23
|
+
export interface ChannelReader { read(query: ChannelReadQuery, signal?: AbortSignal): Promise<ChannelReadPage>; resolve(revision: number, signal?: AbortSignal): Promise<ChannelResolveResult> }
|
|
24
|
+
const prefix = (channel: ChannelRef): string => `${channel.channelId}|`;
|
|
25
|
+
const index = (number: number): string => { requireSafeInteger(number, 0); return String(number).padStart(16, '0'); };
|
|
26
|
+
const channelKey = (channel: ChannelRef): RecordKey => ({ collection: 'channels', key: channel.channelId });
|
|
27
|
+
const postKey = (channel: ChannelRef, sequence: number): RecordKey => ({ collection: 'channel_posts', key: `${prefix(channel)}${index(sequence)}` });
|
|
28
|
+
const descriptorKey = (channel: ChannelRef, revision: number): RecordKey => ({ collection: 'channel_descriptors', key: `${prefix(channel)}${index(revision)}` });
|
|
29
|
+
const eventKey = (channel: ChannelRef, sequence: number): RecordKey => ({ collection: 'channel_events', key: `${prefix(channel)}${index(sequence)}` });
|
|
30
|
+
const put = (key: RecordKey, value: JsonObject): StoreMutation => ({ ...key, kind: 'put', value });
|
|
31
|
+
const fingerprint = (value: JsonObject | undefined): string => canonicalJson(value ?? null);
|
|
32
|
+
const sameRows = (a: readonly StoredRecord[], b: readonly StoredRecord[]): boolean => a.length === b.length && a.every((row, i) => row.key === b[i]!.key && fingerprint(row.value) === fingerprint(b[i]!.value));
|
|
33
|
+
export function channelOperationKey(operation: ChannelOperation): RecordKey {
|
|
34
|
+
const target = operation.payload.kind === 'edit' || operation.payload.kind === 'delete' ? `post:${index(operation.payload.value.targetSequence)}` : 'channel';
|
|
35
|
+
return { collection: 'channel_operations', key: `${prefix(operation.channel)}${target}` };
|
|
36
|
+
}
|
|
37
|
+
export function encodeChannelOperation(operation: ChannelOperation): JsonObject { return { channelId: operation.channel.channelId, relayId: operation.channel.relayId, method: operation.method, payload: channelPayloadCodec.encode(operation.payload), ...(operation.acceptedSequence === undefined ? {} : { acceptedSequence: operation.acceptedSequence }) }; }
|
|
38
|
+
export function readChannelOperation(row: StoredRecord): ChannelOperation {
|
|
39
|
+
const value = row.value; const channel = { channelId: String(value.channelId), relayId: String(value.relayId) }; validateChannelRef(channel);
|
|
40
|
+
const payload = channelPayloadCodec.decode(value.payload!); const method = value.method as ChannelOperationMethod;
|
|
41
|
+
const expected = payload.kind === 'descriptor' ? ['channel.create', 'channel.update', 'channel.close'] : payload.kind === 'post' ? ['channel.post'] : [`channel.post.${payload.kind}`];
|
|
42
|
+
if (value.acceptedSequence !== undefined) { requireSafeInteger(value.acceptedSequence, 1); if (method !== 'channel.post') throw new ProtocolError('invalid_storage', 'Only a publication has an accepted sequence.'); }
|
|
43
|
+
const result = { channel, method, payload, ...(value.acceptedSequence === undefined ? {} : { acceptedSequence: value.acceptedSequence }) };
|
|
44
|
+
if (!expected.includes(method) || payload.value.channelId !== channel.channelId || channelOperationKey(result).key !== row.key) throw new ProtocolError('invalid_storage', 'Stored channel operation has inconsistent method or resource binding.');
|
|
45
|
+
return result;
|
|
46
|
+
}
|
|
47
|
+
function decodeChannel(value: JsonObject | undefined, channel: ChannelRef): ChannelRecord {
|
|
48
|
+
if (!value) return { channel, syncSequence: -1, isFollowed: false };
|
|
49
|
+
if (value.relayId !== channel.relayId || typeof value.isFollowed !== 'boolean') throw new ProtocolError('invalid_binding', 'Stored channel belongs to another relay or is malformed.');
|
|
50
|
+
requireSafeInteger(value.syncSequence, -1);
|
|
51
|
+
return { channel, syncSequence: value.syncSequence, isFollowed: value.isFollowed, ...(value.descriptor ? { descriptor: channelDescriptorCodec.decode(value.descriptor) } : {}) };
|
|
52
|
+
}
|
|
53
|
+
function encodeChannel(value: ChannelRecord): JsonObject { return { relayId: value.channel.relayId, syncSequence: value.syncSequence, isFollowed: value.isFollowed, ...(value.descriptor ? { descriptor: channelDescriptorCodec.encode(value.descriptor) } : {}) }; }
|
|
54
|
+
function channelInfo(value: ChannelRecord): ChannelInfo { return { ref: value.channel, isFollowed: value.isFollowed, ...(value.descriptor ? { descriptor: value.descriptor } : {}) }; }
|
|
55
|
+
function decodePost(value: JsonObject): PostRecord {
|
|
56
|
+
requireSafeInteger(value.sequence, 1); requireSafeInteger(value.appliedThrough, 0);
|
|
57
|
+
if (typeof value.isDeleted !== 'boolean') throw new ProtocolError('invalid_storage', 'Malformed channel post deletion marker.');
|
|
58
|
+
if (value.acceptedAt !== undefined) requireSafeInteger(value.acceptedAt, 0);
|
|
59
|
+
return { sequence: value.sequence, appliedThrough: value.appliedThrough, isDeleted: value.isDeleted,
|
|
60
|
+
...(value.messageId === undefined ? {} : { messageId: String(value.messageId), author: String(value.author), acceptedAt: value.acceptedAt as number }),
|
|
61
|
+
...(value.original ? { original: channelPostCodec.decode(value.original) } : {}), ...(value.post ? { post: channelPostCodec.decode(value.post) } : {}) };
|
|
62
|
+
}
|
|
63
|
+
function encodePost(value: PostRecord): JsonObject { return { sequence: value.sequence, appliedThrough: value.appliedThrough, isDeleted: value.isDeleted,
|
|
64
|
+
...(value.messageId === undefined ? {} : { messageId: value.messageId, author: value.author!, acceptedAt: value.acceptedAt! }),
|
|
65
|
+
...(value.original ? { original: channelPostCodec.encode(value.original) } : {}), ...(value.post ? { post: channelPostCodec.encode(value.post) } : {}) }; }
|
|
66
|
+
function postInfo(record: PostRecord, channel: ChannelRef): ChannelPostInfo | undefined {
|
|
67
|
+
if (record.isDeleted || !record.post) return undefined;
|
|
68
|
+
if (!record.messageId || !record.author || record.acceptedAt === undefined) throw new ProtocolError('invalid_storage', 'Visible post lacks its original publication metadata.');
|
|
69
|
+
return { ref: { channel, sequence: record.sequence }, messageId: record.messageId, author: record.author, acceptedAt: record.acceptedAt,
|
|
70
|
+
...(record.post.body ? { body: record.post.body } : {}), ...(record.post.attachments ? { attachments: record.post.attachments } : {}) };
|
|
71
|
+
}
|
|
72
|
+
function target(event: ChannelEvent): number | undefined { return event.payload.kind === 'post' ? event.sequence : event.payload.kind === 'edit' || event.payload.kind === 'delete' ? event.payload.value.targetSequence : undefined; }
|
|
73
|
+
function storeDescriptor(record: ChannelRecord, result: ChannelResolveResult, known: Map<number, ChannelResolveResult>): ChannelRecord {
|
|
74
|
+
const next = result.descriptor; const old = known.get(next.revision);
|
|
75
|
+
if (old && channelDescriptorCodec.stringify(old.descriptor) !== channelDescriptorCodec.stringify(next)) throw new ProtocolError('conflicting_revision', 'Channel descriptor conflicts with an already verified revision.');
|
|
76
|
+
const previous = record.descriptor;
|
|
77
|
+
if (previous && (previous.creator !== next.creator || previous.createdAt !== next.createdAt || canonicalJson([...previous.nonce]) !== canonicalJson([...next.nonce]))) throw new ProtocolError('immutable_field', 'Immutable channel fields changed.');
|
|
78
|
+
if (previous?.status === 'closed' && next.revision > previous.revision) throw new ProtocolError('closed_channel', 'A closed channel cannot have a later descriptor revision.');
|
|
79
|
+
known.set(next.revision, result); return previous && next.revision <= previous.revision ? record : { ...record, descriptor: next };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Verified evidence and projections commit together; all network reads occur before the CAS transaction. */
|
|
83
|
+
export class ChannelRepository {
|
|
84
|
+
constructor(readonly store: MeshlineStore, readonly context: NetworkContext, readonly accountId: string) {}
|
|
85
|
+
async get(channel: ChannelRef, signal?: AbortSignal): Promise<ChannelRecord> { validateChannelRef(channel); return decodeChannel((await this.store.read([channelKey(channel)], signal)).sets[0]![0]?.value, channel); }
|
|
86
|
+
async post(post: ChannelPostRef, signal?: AbortSignal): Promise<ChannelPostInfo | undefined> {
|
|
87
|
+
validateChannelPostRef(post); const rows = await this.store.read([channelKey(post.channel), postKey(post.channel, post.sequence)], signal); decodeChannel(rows.sets[0]![0]?.value, post.channel);
|
|
88
|
+
const row = rows.sets[1]![0]; return row && postInfo(decodePost(row.value), post.channel);
|
|
89
|
+
}
|
|
90
|
+
async posts(channelId?: string, author?: string, signal?: AbortSignal): Promise<QueryReader<ChannelPostInfo>> {
|
|
91
|
+
const rows = await this.store.read([{ collection: 'channel_posts', ...(channelId ? { prefix: `${channelId}|` } : {}) }, { collection: 'channels' }], signal);
|
|
92
|
+
const channels = new Map(rows.sets[1]!.map(row => [row.key, { channelId: row.key, relayId: String(row.value.relayId) }]));
|
|
93
|
+
const values = rows.sets[0]!.map(row => postInfo(decodePost(row.value), channels.get(row.key.split('|')[0]!)!)).filter((value): value is ChannelPostInfo => value !== undefined && (author === undefined || value.author === author));
|
|
94
|
+
return snapshotReader(values);
|
|
95
|
+
}
|
|
96
|
+
async followed(relayId?: string, signal?: AbortSignal): Promise<readonly ChannelRecord[]> {
|
|
97
|
+
const rows = (await this.store.read([{ collection: 'channels' }], signal)).sets[0]!;
|
|
98
|
+
return rows.filter(row => row.value.isFollowed && (relayId === undefined || row.value.relayId === relayId)).map(row => decodeChannel(row.value, { channelId: row.key, relayId: String(row.value.relayId) }));
|
|
99
|
+
}
|
|
100
|
+
async follow(channel: ChannelRef, followed: boolean, signal?: AbortSignal): Promise<boolean> {
|
|
101
|
+
return updateStore(this.store, [channelKey(channel)], snapshot => {
|
|
102
|
+
const row = snapshot.sets[0]![0]; if (!row) return { mutations: [], result: false }; const current = decodeChannel(row.value, channel);
|
|
103
|
+
return { mutations: current.isFollowed === followed ? [] : [put(channelKey(channel), encodeChannel({ ...current, isFollowed: followed }))], result: current.isFollowed !== followed };
|
|
104
|
+
}, signal);
|
|
105
|
+
}
|
|
106
|
+
async saveDescriptor(channel: ChannelRef, result: ChannelResolveResult, operation?: ChannelOperation, signal?: AbortSignal): Promise<ChannelInfo | undefined> {
|
|
107
|
+
verifyChannelDescriptor(result, this.context, channel); const key = descriptorKey(channel, result.descriptor.revision);
|
|
108
|
+
return updateStore(this.store, [channelKey(channel), key, ...(operation ? [channelOperationKey(operation)] : [])], snapshot => {
|
|
109
|
+
const before = decodeChannel(snapshot.sets[0]![0]?.value, channel); const known = new Map<number, ChannelResolveResult>();
|
|
110
|
+
if (snapshot.sets[1]![0]) known.set(result.descriptor.revision, channelResolveResultCodec.decode(snapshot.sets[1]![0]!.value));
|
|
111
|
+
const current = storeDescriptor(before, result, known); const mutations = [put(channelKey(channel), encodeChannel(current)), put(key, channelResolveResultCodec.encode(result))];
|
|
112
|
+
if (operation && fingerprint(snapshot.sets[2]![0]?.value) === canonicalJson(encodeChannelOperation(operation))) mutations.push({ kind: 'delete', ...channelOperationKey(operation) });
|
|
113
|
+
return { mutations, result: current !== before ? channelInfo(current) : undefined };
|
|
114
|
+
}, signal);
|
|
115
|
+
}
|
|
116
|
+
async saveOperation(operation: ChannelOperation, signal?: AbortSignal): Promise<void> {
|
|
117
|
+
validateChannelRef(operation.channel); validateChannelPayload(operation.payload, this.context); const key = channelOperationKey(operation);
|
|
118
|
+
await updateStore(this.store, [key], snapshot => {
|
|
119
|
+
if (snapshot.sets[0]!.length) throw new StateConflictError('An earlier operation on this channel resource remains unfinished. Start the component to recover it.');
|
|
120
|
+
return { mutations: [put(key, encodeChannelOperation(operation))], result: undefined };
|
|
121
|
+
}, signal);
|
|
122
|
+
}
|
|
123
|
+
async operations(signal?: AbortSignal): Promise<readonly ChannelOperation[]> { return (await this.store.read([{ collection: 'channel_operations' }], signal)).sets[0]!.map(readChannelOperation); }
|
|
124
|
+
async hasOperation(operation: ChannelOperation, signal?: AbortSignal): Promise<boolean> { return fingerprint((await this.store.read([channelOperationKey(operation)], signal)).sets[0]![0]?.value) === canonicalJson(encodeChannelOperation(operation)); }
|
|
125
|
+
async acknowledgePublication(operation: ChannelOperation, sequence: number): Promise<ChannelOperation> {
|
|
126
|
+
requireSafeInteger(sequence, 1); if (operation.method !== 'channel.post') throw new ProtocolError('invalid_operation', 'Only publications return sequence acknowledgements.');
|
|
127
|
+
const accepted = { ...operation, acceptedSequence: sequence }; const key = channelOperationKey(operation);
|
|
128
|
+
await updateStore(this.store, [key], snapshot => {
|
|
129
|
+
if (fingerprint(snapshot.sets[0]![0]?.value) !== canonicalJson(encodeChannelOperation(operation))) throw new StateConflictError('Publication changed before acknowledgement could be saved.');
|
|
130
|
+
return { mutations: [put(key, encodeChannelOperation(accepted))], result: undefined };
|
|
131
|
+
}); return accepted;
|
|
132
|
+
}
|
|
133
|
+
async confirmPublication(channel: ChannelRef, sequence: number, request: ChannelPost, signal?: AbortSignal): Promise<void> {
|
|
134
|
+
const value = (await this.store.read([postKey(channel, sequence)], signal)).sets[0]![0]?.value;
|
|
135
|
+
if (!value) throw new ProtocolError('missing_publication', 'Accepted publication is absent from the verified timeline.'); const post = decodePost(value);
|
|
136
|
+
if (post.messageId !== undefined && (post.messageId !== request.messageId || post.author !== this.accountId || post.original && channelPostCodec.stringify(post.original) !== channelPostCodec.stringify(request))) throw new ProtocolError('conflicting_publication', 'Acknowledged sequence identifies another publication.');
|
|
137
|
+
}
|
|
138
|
+
async completeOperation(operation: ChannelOperation, deletion = false, signal?: AbortSignal): Promise<ChannelPostChange | undefined> {
|
|
139
|
+
const channel = operation.channel; const sequence = deletion && operation.payload.kind === 'delete' ? operation.payload.value.targetSequence : undefined;
|
|
140
|
+
return updateStore(this.store, [channelOperationKey(operation), channelKey(channel), ...(sequence ? [postKey(channel, sequence)] : [])], snapshot => {
|
|
141
|
+
const mutations: StoreMutation[] = []; let result: ChannelPostChange | undefined;
|
|
142
|
+
if (fingerprint(snapshot.sets[0]![0]?.value) !== canonicalJson(encodeChannelOperation(operation))) return { mutations, result };
|
|
143
|
+
mutations.push({ kind: 'delete', ...channelOperationKey(operation) }, put(channelKey(channel), encodeChannel(decodeChannel(snapshot.sets[1]![0]?.value, channel))));
|
|
144
|
+
if (sequence !== undefined) {
|
|
145
|
+
const previous = snapshot.sets[2]![0]?.value; const post = previous ? decodePost(previous) : { sequence, appliedThrough: sequence, isDeleted: false };
|
|
146
|
+
if (!post.isDeleted && post.post) result = { ref: { channel, sequence }, kind: 'deleted' };
|
|
147
|
+
post.isDeleted = true; delete post.post; mutations.push(put(postKey(channel, sequence), encodePost(post)));
|
|
148
|
+
}
|
|
149
|
+
return { mutations, result };
|
|
150
|
+
}, signal);
|
|
151
|
+
}
|
|
152
|
+
async publication(channel: ChannelRef, request: ChannelPost, signal?: AbortSignal): Promise<number | undefined> {
|
|
153
|
+
const rows = (await this.store.read([{ collection: 'channel_posts', prefix: prefix(channel) }], signal)).sets[0]!;
|
|
154
|
+
const post = rows.map(row => decodePost(row.value)).find(value => value.messageId === request.messageId && value.author === this.accountId);
|
|
155
|
+
if (post?.original && channelPostCodec.stringify(post.original) !== channelPostCodec.stringify(request)) throw new ProtocolError('conflicting_publication', 'Synchronized publication differs from the exact pending request.'); return post?.sequence;
|
|
156
|
+
}
|
|
157
|
+
async readPage(channel: ChannelRef, query: ChannelReadQuery, reader: ChannelReader, advance: boolean, signal?: AbortSignal): Promise<{ page: ChannelReadPage; changes: ChannelChanges }> {
|
|
158
|
+
validateChannelRef(channel); validateChannelReadQuery(query); if (query.channelId !== channel.channelId) throw new ProtocolError('invalid_binding', 'History query belongs to another channel.');
|
|
159
|
+
const queries: RecordQuery[] = [channelKey(channel), ...['channel_posts', 'channel_descriptors', 'channel_events', 'channel_operations'].map(collection => ({ collection, prefix: prefix(channel) }))];
|
|
160
|
+
const snapshot = await this.store.read(queries, signal); let record = decodeChannel(snapshot.sets[0]![0]?.value, channel); const beforeChannel = record;
|
|
161
|
+
const known = new Map(snapshot.sets[2]!.map(row => { const result = channelResolveResultCodec.decode(row.value); verifyChannelDescriptor(result, this.context, channel); return [result.descriptor.revision, result] as const; }));
|
|
162
|
+
const descriptors = new Map<number, ChannelResolveResult>(); const evidence = new Map<number, { event: ChannelEvent; certificate: DeviceCertificate }>();
|
|
163
|
+
const applied = new Set<number>();
|
|
164
|
+
const read = async (selection: ChannelReadQuery): Promise<ChannelReadPage> => {
|
|
165
|
+
const page = channelReadPageCodec.decode(channelReadPageCodec.encode(await reader.read(selection, signal))); const certificates = validateChannelReadPage(page, selection, this.context);
|
|
166
|
+
for (const entry of page.events) if (entry.payload.kind === 'descriptor') {
|
|
167
|
+
const resolved = { descriptor: entry.payload.value, signerCertificate: certificates.get(entry.signerDeviceId)! }; verifyChannelDescriptor(resolved, this.context, channel);
|
|
168
|
+
const previous = descriptors.get(entry.descriptorRev) ?? known.get(entry.descriptorRev);
|
|
169
|
+
if (previous && channelDescriptorCodec.stringify(previous.descriptor) !== channelDescriptorCodec.stringify(resolved.descriptor)) throw new ProtocolError('conflicting_revision', 'Timeline descriptor conflicts with verified evidence.'); descriptors.set(entry.descriptorRev, resolved);
|
|
170
|
+
}
|
|
171
|
+
for (const entry of page.events) {
|
|
172
|
+
let resolved = descriptors.get(entry.descriptorRev) ?? known.get(entry.descriptorRev);
|
|
173
|
+
if (!resolved) { resolved = await reader.resolve(entry.descriptorRev, signal); verifyChannelDescriptor(resolved, this.context, channel); if (resolved.descriptor.revision !== entry.descriptorRev) throw new ProtocolError('invalid_revision', 'Relay returned a different descriptor revision.'); }
|
|
174
|
+
descriptors.set(entry.descriptorRev, resolved); const certificate = certificates.get(entry.signerDeviceId)!; verifyChannelEvent(entry, certificate, resolved.descriptor, this.context, channel);
|
|
175
|
+
const previous = evidence.get(entry.sequence)?.event ?? (snapshot.sets[3]!.find(row => row.key === eventKey(channel, entry.sequence).key)?.value.event ? channelEventCodec.decode(snapshot.sets[3]!.find(row => row.key === eventKey(channel, entry.sequence).key)!.value.event!) : undefined);
|
|
176
|
+
if (previous && channelEventCodec.stringify(previous) !== channelEventCodec.stringify(entry)) throw new ProtocolError('conflicting_event', 'Timeline sequence conflicts with previously verified evidence.'); evidence.set(entry.sequence, { event: entry, certificate });
|
|
177
|
+
}
|
|
178
|
+
return page;
|
|
179
|
+
};
|
|
180
|
+
const page = await read(query); const posts = new Map<number, PostRecord>(); const originals = new Map<number, PostRecord | undefined>();
|
|
181
|
+
const boundary = query.after ?? (page.events[0]?.sequence ?? 0) - 1;
|
|
182
|
+
for (const event of page.events) {
|
|
183
|
+
const sequence = target(event); if (sequence === undefined || posts.has(sequence)) continue;
|
|
184
|
+
const saved = snapshot.sets[1]!.find(row => row.key === postKey(channel, sequence).key)?.value; const before = saved && decodePost(saved); originals.set(sequence, before);
|
|
185
|
+
const post: PostRecord = saved ? decodePost(saved) : { sequence, appliedThrough: sequence - 1, isDeleted: false }; posts.set(sequence, post);
|
|
186
|
+
const related = page.events.filter(entry => target(entry) === sequence);
|
|
187
|
+
const deletion = related.find(entry => entry.payload.kind === 'delete');
|
|
188
|
+
if (deletion) { post.isDeleted = true; delete post.post; post.appliedThrough = Math.max(post.appliedThrough, deletion.sequence); continue; }
|
|
189
|
+
if (post.isDeleted) continue; if (post.post) post.appliedThrough = Math.max(post.appliedThrough, record.syncSequence);
|
|
190
|
+
if (related.some(entry => entry.payload.kind === 'edit' && entry.sequence > post.appliedThrough) && !related.some(entry => entry.payload.kind === 'post') && post.appliedThrough < boundary) {
|
|
191
|
+
let after = post.appliedThrough;
|
|
192
|
+
for (;;) { const caught = await read({ channelId: channel.channelId, after });
|
|
193
|
+
for (const entry of caught.events) if (target(entry) === sequence && (entry.sequence <= boundary || entry.payload.kind === 'delete')) {
|
|
194
|
+
this.#apply(post, entry, evidence.get(entry.sequence)!.certificate); applied.add(entry.sequence);
|
|
195
|
+
}
|
|
196
|
+
if (!caught.hasMore || caught.events.at(-1)!.sequence >= boundary) break; after = caught.events.at(-1)!.sequence;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
for (const entry of page.events) { const sequence = target(entry); if (sequence !== undefined) { this.#apply(posts.get(sequence)!, entry, evidence.get(entry.sequence)!.certificate); applied.add(entry.sequence); } }
|
|
201
|
+
const changes: ChannelPostChange[] = []; const mutations: StoreMutation[] = [];
|
|
202
|
+
for (const post of posts.values()) {
|
|
203
|
+
if (!post.isDeleted && !post.post) throw new ProtocolError('missing_original', 'Original channel post is unavailable; the page remains uncommitted.');
|
|
204
|
+
post.appliedThrough = Math.max(post.appliedThrough, page.events.at(-1)!.sequence); const before = originals.get(post.sequence); const info = postInfo(post, channel);
|
|
205
|
+
if (!before?.isDeleted) { if (post.isDeleted && before?.post) changes.push({ ref: { channel, sequence: post.sequence }, kind: 'deleted' });
|
|
206
|
+
else if (info && fingerprint(before?.post && channelPostCodec.encode(before.post)) !== fingerprint(post.post && channelPostCodec.encode(post.post))) changes.push({ ref: info.ref, kind: before?.post ? 'edited' : 'added', info }); }
|
|
207
|
+
mutations.push(put(postKey(channel, post.sequence), encodePost(post)));
|
|
208
|
+
}
|
|
209
|
+
for (const result of [...descriptors.values()].sort((a, b) => a.descriptor.revision - b.descriptor.revision)) { record = storeDescriptor(record, result, known); mutations.push(put(descriptorKey(channel, result.descriptor.revision), channelResolveResultCodec.encode(result))); }
|
|
210
|
+
if (advance && page.events.length) record = { ...record, syncSequence: Math.max(record.syncSequence, page.events.at(-1)!.sequence) };
|
|
211
|
+
mutations.push(put(channelKey(channel), encodeChannel(record)));
|
|
212
|
+
for (const { event, certificate } of evidence.values()) {
|
|
213
|
+
mutations.push(put(eventKey(channel, event.sequence), { event: channelEventCodec.encode(event) }));
|
|
214
|
+
// Catch-up verifies whole pages, but may project only an earlier subset
|
|
215
|
+
// for selected posts. Confirmation must commit with the affected projection.
|
|
216
|
+
if (!applied.has(event.sequence)) continue;
|
|
217
|
+
for (const row of snapshot.sets[4]!) {
|
|
218
|
+
const operation = readChannelOperation(row); const payload = operation.payload;
|
|
219
|
+
if (payload.kind === 'post' && event.payload.kind === 'post' && payload.value.messageId === event.payload.value.messageId && certificate.account === this.accountId) {
|
|
220
|
+
if (operation.acceptedSequence !== undefined && operation.acceptedSequence !== event.sequence) throw new ProtocolError('invalid_sequence', 'Publication acknowledgement differs from its verified timeline sequence.');
|
|
221
|
+
if (canonicalJson(channelPayloadCodec.encode(payload)) !== canonicalJson(channelPayloadCodec.encode(event.payload))) throw new ProtocolError('conflicting_publication', 'Synchronized post conflicts with the pending publication.'); mutations.push({ ...channelOperationKey(operation), kind: 'delete' });
|
|
222
|
+
} else if ((payload.kind === 'edit' || payload.kind === 'delete') && payload.kind === event.payload.kind && target(event) === payload.value.targetSequence
|
|
223
|
+
&& (payload.kind === 'delete' || canonicalJson(channelPayloadCodec.encode(payload)) === canonicalJson(channelPayloadCodec.encode(event.payload)))) mutations.push({ ...channelOperationKey(operation), kind: 'delete' });
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
const unique = [...new Map(mutations.map(mutation => [`${mutation.collection}|${mutation.key}`, mutation])).values()];
|
|
227
|
+
await updateStore(this.store, queries, current => {
|
|
228
|
+
if (snapshot.sets.some((rows, i) => !sameRows(rows, current.sets[i]!))) throw new StateConflictError('Channel state changed while history was being verified; retry with current state.');
|
|
229
|
+
return { mutations: unique, result: undefined };
|
|
230
|
+
}, signal);
|
|
231
|
+
return { page, changes: { posts: changes, ...(record.descriptor !== beforeChannel.descriptor ? { channel: channelInfo(record) } : {}) } };
|
|
232
|
+
}
|
|
233
|
+
#apply(post: PostRecord, entry: ChannelEvent, certificate: DeviceCertificate): void {
|
|
234
|
+
const payload = entry.payload;
|
|
235
|
+
if (payload.kind === 'post') {
|
|
236
|
+
if (post.messageId !== undefined) { if (post.messageId !== payload.value.messageId || post.author !== certificate.account || post.original && channelPostCodec.stringify(post.original) !== channelPostCodec.stringify(payload.value)) throw new ProtocolError('conflicting_post', 'Original channel publication changed.'); return; }
|
|
237
|
+
post.messageId = payload.value.messageId; post.author = certificate.account; post.acceptedAt = entry.acceptedAt; post.original = payload.value; if (!post.isDeleted) post.post = payload.value;
|
|
238
|
+
} else if (payload.kind === 'delete') { post.isDeleted = true; delete post.post; }
|
|
239
|
+
else if (payload.kind === 'edit' && !post.isDeleted && entry.sequence > post.appliedThrough) {
|
|
240
|
+
if (!post.post) throw new ProtocolError('missing_original', 'Original channel post is unavailable; the page remains uncommitted.'); post.post = applyChannelPostEdit(post.post, payload.value);
|
|
241
|
+
}
|
|
242
|
+
post.appliedThrough = Math.max(post.appliedThrough, entry.sequence);
|
|
243
|
+
}
|
|
244
|
+
}
|