@powerhousedao/reactor-attachments 6.2.3-dev.6 → 6.2.3-dev.60
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/attachment-schema-compiler-rIaJpiHO.js +262 -0
- package/dist/attachment-schema-compiler-rIaJpiHO.js.map +1 -0
- package/dist/client.d.ts +56 -36
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +33 -20
- package/dist/client.js.map +1 -1
- package/dist/hash-BobbR4r4.js +63 -0
- package/dist/hash-BobbR4r4.js.map +1 -0
- package/dist/index.d.ts +81 -111
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +133 -405
- package/dist/index.js.map +1 -1
- package/dist/{null-attachment-transport-CMrO_ZKA.d.ts → interfaces-C4GRlKeB.d.ts} +34 -327
- package/dist/interfaces-C4GRlKeB.d.ts.map +1 -0
- package/dist/null-attachment-transport-Bs4JpmFx.d.ts +327 -0
- package/dist/null-attachment-transport-Bs4JpmFx.d.ts.map +1 -0
- package/dist/{null-attachment-transport-CrsUafCi.js → null-attachment-transport-Cm_kb5ww.js} +37 -283
- package/dist/null-attachment-transport-Cm_kb5ww.js.map +1 -0
- package/dist/replication.d.ts +955 -0
- package/dist/replication.d.ts.map +1 -0
- package/dist/replication.js +1975 -0
- package/dist/replication.js.map +1 -0
- package/dist/switchboard-attachment-transport-BH3ZAmxE.js +284 -0
- package/dist/switchboard-attachment-transport-BH3ZAmxE.js.map +1 -0
- package/dist/types-AagIcjTD.d.ts +61 -0
- package/dist/types-AagIcjTD.d.ts.map +1 -0
- package/package.json +16 -7
- package/dist/null-attachment-transport-CMrO_ZKA.d.ts.map +0 -1
- package/dist/null-attachment-transport-CrsUafCi.js.map +0 -1
|
@@ -0,0 +1,955 @@
|
|
|
1
|
+
import { O as TransportFetchResult, _ as AttachmentResponse, a as IAttachmentTransport, g as AttachmentMetadata, h as AttachmentHeader, i as IAttachmentStore } from "./interfaces-C4GRlKeB.js";
|
|
2
|
+
import { a as IAttachmentReferenceScanner, c as IAttachmentSchemaCompiler, i as IAttachmentReferenceReader, n as AttachmentReferencePageResult, r as AttachmentReferenceRow } from "./types-AagIcjTD.js";
|
|
3
|
+
import { AttachmentHash, AttachmentRef, IDocumentModelRegistry, IEventBus, IReactorClient, ISyncManager, JwtHandler, LocalChannelPort } from "@powerhousedao/reactor";
|
|
4
|
+
import { AuthSubject, OperationWithContext } from "@powerhousedao/shared/document-model";
|
|
5
|
+
//#region src/access/attachment-read-gate.d.ts
|
|
6
|
+
/** The reactor's read gate, as an attachment read consults it. */
|
|
7
|
+
type AttachmentReadGate = Pick<IReactorClient, "isServed" | "get">;
|
|
8
|
+
/** Scopes a subject may read, by document id; `SyncScopeGate` satisfies it. */
|
|
9
|
+
interface IDocumentScopeGate {
|
|
10
|
+
scopePredicateById(documentId: string, subject: {
|
|
11
|
+
address?: string;
|
|
12
|
+
key?: string;
|
|
13
|
+
}, branch: string, signal?: AbortSignal): Promise<(scope: string) => boolean>;
|
|
14
|
+
}
|
|
15
|
+
/** The scope an attachment's bytes belong to: the document's own domain state. */
|
|
16
|
+
export declare const ATTACHMENT_READ_SCOPE = "global";
|
|
17
|
+
/** The branch an attachment reference is resolved against. */
|
|
18
|
+
export declare const ATTACHMENT_READ_BRANCH = "main";
|
|
19
|
+
/** Whether `value` is a canonical v1 hash: 64 lowercase SHA-256 hex chars. */
|
|
20
|
+
export declare function isAttachmentHash(value: unknown): value is AttachmentHash;
|
|
21
|
+
/** Rethrows a failure: a read side that is down must not read as a denial. */
|
|
22
|
+
export declare function scopeGateAllowsAttachmentRead(scopeGate: IDocumentScopeGate, documentId: string, subject: AuthSubject): Promise<boolean>;
|
|
23
|
+
/** Served to the subject, references `ref`, and a referencing scope is readable. */
|
|
24
|
+
export declare function readGateAllowsAttachmentRead(readGate: AttachmentReadGate, references: Pick<IAttachmentReferenceReader, "referencingScopes">, documentId: string, ref: AttachmentRef, subject: AuthSubject): Promise<boolean>;
|
|
25
|
+
//#endregion
|
|
26
|
+
//#region src/local/local-attachment-server.d.ts
|
|
27
|
+
/** The peer a server answers: which reactor, over which channel. */
|
|
28
|
+
type AttachmentPeerLink = {
|
|
29
|
+
peerId: string;
|
|
30
|
+
channelName: string;
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Decides whether the linked peer may read `hash` through `documentId`. False
|
|
34
|
+
* is answered as `not-found`, indistinguishable from absent bytes.
|
|
35
|
+
*/
|
|
36
|
+
type LocalAttachmentAuthorizer = (link: AttachmentPeerLink, hash: AttachmentHash, documentId: string) => Promise<boolean>;
|
|
37
|
+
type LocalAttachmentServerOptions = {
|
|
38
|
+
/** The brokered port to the peer; the same one the transport half uses. */
|
|
39
|
+
port: LocalChannelPort;
|
|
40
|
+
/** Who is asking; handed to {@link authorize}. */
|
|
41
|
+
link: AttachmentPeerLink;
|
|
42
|
+
/** The store bytes are served FROM. Only locally held bytes are served. */
|
|
43
|
+
store: IAttachmentStore;
|
|
44
|
+
/** Defaults to refusing every read; see {@link readGateAttachmentAuthorizer}. */
|
|
45
|
+
authorize?: LocalAttachmentAuthorizer;
|
|
46
|
+
/** Bytes per `chunk` message; defaults to {@link DEFAULT_LOCAL_CHUNK_BYTES}. */
|
|
47
|
+
chunkSizeBytes?: number;
|
|
48
|
+
/** Requests served at once; one past it is answered `pending`. */
|
|
49
|
+
maxConcurrentServes?: number;
|
|
50
|
+
onDiagnostic?: (message: string, error?: unknown) => void;
|
|
51
|
+
};
|
|
52
|
+
/** Default {@link LocalAttachmentServerOptions.maxConcurrentServes}. */
|
|
53
|
+
export declare const DEFAULT_LOCAL_MAX_CONCURRENT_SERVES = 4;
|
|
54
|
+
type ReadGateAttachmentAuthorizerOptions = {
|
|
55
|
+
readGate: AttachmentReadGate;
|
|
56
|
+
/** `SyncScopeGate`; a reactor without a policy model passes one over `BareReadGate`. */
|
|
57
|
+
scopeGate: IDocumentScopeGate;
|
|
58
|
+
references: Pick<IAttachmentReferenceReader, "referencingScopes">;
|
|
59
|
+
/** The subject a link reads as; undefined refuses every read on it. */
|
|
60
|
+
subjectOf: (link: AttachmentPeerLink) => AuthSubject | undefined;
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* Authorizes a peer read the way the Switchboard's attachment route does: the
|
|
64
|
+
* link's subject may read the document's `global` scope, the reactor serves
|
|
65
|
+
* the document to it, and a scope it may read references the hash.
|
|
66
|
+
*/
|
|
67
|
+
export declare function readGateAttachmentAuthorizer(options: ReadGateAttachmentAuthorizerOptions): LocalAttachmentAuthorizer;
|
|
68
|
+
/**
|
|
69
|
+
* Serves attachment bytes to a brokered local peer from this reactor's own
|
|
70
|
+
* store.
|
|
71
|
+
*
|
|
72
|
+
* The answering half of {@link LocalAttachmentTransport}; both run on the same
|
|
73
|
+
* `LocalChannelPort` and ignore each other's messages.
|
|
74
|
+
*
|
|
75
|
+
* It never chains. The store is read WITHOUT a document id, which means a
|
|
76
|
+
* `LocalAttachmentStore` serves only bytes it actually holds and cannot reach
|
|
77
|
+
* for its own transport on the requester's behalf. Without that, two peers
|
|
78
|
+
* linked to each other would answer a miss by asking each other, and a hash
|
|
79
|
+
* neither held would bounce between them.
|
|
80
|
+
*/
|
|
81
|
+
export declare class LocalAttachmentServer {
|
|
82
|
+
private readonly port;
|
|
83
|
+
private readonly link;
|
|
84
|
+
private readonly store;
|
|
85
|
+
private readonly authorize;
|
|
86
|
+
private readonly chunkSizeBytes;
|
|
87
|
+
private readonly maxConcurrentServes;
|
|
88
|
+
private readonly onDiagnostic;
|
|
89
|
+
private readonly cancelled;
|
|
90
|
+
private readonly inFlight;
|
|
91
|
+
private readonly detachPort;
|
|
92
|
+
private served;
|
|
93
|
+
private bytesServed;
|
|
94
|
+
private refused;
|
|
95
|
+
private closed;
|
|
96
|
+
constructor(options: LocalAttachmentServerOptions);
|
|
97
|
+
/** What this peer has handed out. */
|
|
98
|
+
stats(): {
|
|
99
|
+
served: number;
|
|
100
|
+
bytesServed: number;
|
|
101
|
+
refused: number;
|
|
102
|
+
};
|
|
103
|
+
/** Detaches from the port. In-flight serves stop at their next slice. */
|
|
104
|
+
close(): void;
|
|
105
|
+
private onMessage;
|
|
106
|
+
private stopped;
|
|
107
|
+
private serve;
|
|
108
|
+
private serveInner;
|
|
109
|
+
private stream;
|
|
110
|
+
/** Refused and absent are one answer, so a peer learns nothing from it. */
|
|
111
|
+
private refuse;
|
|
112
|
+
private failRequest;
|
|
113
|
+
private post;
|
|
114
|
+
}
|
|
115
|
+
//#endregion
|
|
116
|
+
//#region src/local/local-attachment-transport.d.ts
|
|
117
|
+
type LocalAttachmentTransportOptions = {
|
|
118
|
+
/**
|
|
119
|
+
* The brokered port to the peer -- the same `LocalChannelPort` abstraction
|
|
120
|
+
* `LocalChannel` sync runs over (W1.1), on its own channel so byte traffic
|
|
121
|
+
* never interleaves with the sync wire.
|
|
122
|
+
*/
|
|
123
|
+
port: LocalChannelPort;
|
|
124
|
+
/**
|
|
125
|
+
* How long to wait for the NEXT message of a request before abandoning it.
|
|
126
|
+
* Reset by every message, so a slow but progressing transfer is not cut off
|
|
127
|
+
* while a silent peer is. Defaults to
|
|
128
|
+
* {@link DEFAULT_LOCAL_REQUEST_TIMEOUT_MS}.
|
|
129
|
+
*/
|
|
130
|
+
requestTimeoutMs?: number;
|
|
131
|
+
/** Swapped in tests; defaults to the realm's timers. */
|
|
132
|
+
setTimer?: (callback: () => void, delayMs: number) => unknown;
|
|
133
|
+
clearTimer?: (handle: unknown) => void;
|
|
134
|
+
/** Identifies this instance's request ids; defaults to a random nonce. */
|
|
135
|
+
instanceId?: string;
|
|
136
|
+
/** Refuses a body the peer declares larger than this. */
|
|
137
|
+
maxBytes?: number;
|
|
138
|
+
};
|
|
139
|
+
/**
|
|
140
|
+
* `IAttachmentTransport` over a brokered `LocalChannelPort`: one linked reactor
|
|
141
|
+
* pulling attachment bytes straight from its peer, with no Switchboard and no
|
|
142
|
+
* HTTP.
|
|
143
|
+
*
|
|
144
|
+
* The counterpart of {@link LocalAttachmentServer}, and a pair of them runs on
|
|
145
|
+
* each end of one port: a reactor both asks for bytes and serves them. The two
|
|
146
|
+
* halves share the port and ignore each other's messages (see
|
|
147
|
+
* `isLocalAttachmentRequest` / `isLocalAttachmentResponse`), and request ids
|
|
148
|
+
* are namespaced per instance so two peers that both start counting at 1 never
|
|
149
|
+
* match each other's replies.
|
|
150
|
+
*
|
|
151
|
+
* Pull-only by design. The agreed byte-movement model is lazy
|
|
152
|
+
* fetch-on-reference, so {@link announce} is a no-op and {@link push} refuses
|
|
153
|
+
* by name rather than silently succeeding: a push that reported success while
|
|
154
|
+
* moving nothing would make an eager-replication caller believe bytes had
|
|
155
|
+
* landed on a peer.
|
|
156
|
+
*/
|
|
157
|
+
export declare class LocalAttachmentTransport implements IAttachmentTransport {
|
|
158
|
+
private readonly port;
|
|
159
|
+
private readonly requestTimeoutMs;
|
|
160
|
+
private readonly setTimer;
|
|
161
|
+
private readonly clearTimer;
|
|
162
|
+
private readonly instanceId;
|
|
163
|
+
private readonly maxBytes;
|
|
164
|
+
private readonly pending;
|
|
165
|
+
private readonly detachPort;
|
|
166
|
+
private nextRequest;
|
|
167
|
+
private closed;
|
|
168
|
+
constructor(options: LocalAttachmentTransportOptions);
|
|
169
|
+
fetch(hash: AttachmentHash, documentId: string, signal?: AbortSignal): Promise<TransportFetchResult>;
|
|
170
|
+
/**
|
|
171
|
+
* No-op: the agreed model is the peer PULLING on reference, so there is
|
|
172
|
+
* nothing useful to tell it -- it will ask when one of its own operations
|
|
173
|
+
* names the hash. An eager-announce protocol would need a peer that acts on
|
|
174
|
+
* the notice, which nothing does.
|
|
175
|
+
*/
|
|
176
|
+
announce(): Promise<void>;
|
|
177
|
+
/** Refuses: this transport moves bytes only in answer to a peer's request. */
|
|
178
|
+
push(): Promise<void>;
|
|
179
|
+
/** Detaches from the port and fails every in-flight request. */
|
|
180
|
+
close(): void;
|
|
181
|
+
private onMessage;
|
|
182
|
+
private handle;
|
|
183
|
+
/** A reply that breaks the protocol: tell the peer to stop, fail the request. */
|
|
184
|
+
private violate;
|
|
185
|
+
/** Resets this request's silence timer. */
|
|
186
|
+
private arm;
|
|
187
|
+
/** Forgets a request that completed normally. */
|
|
188
|
+
private release;
|
|
189
|
+
/**
|
|
190
|
+
* Forgets a request that failed, pushing the failure to whichever side is
|
|
191
|
+
* still listening: the fetch promise before `begin`, the body stream after.
|
|
192
|
+
*/
|
|
193
|
+
private abandon;
|
|
194
|
+
private post;
|
|
195
|
+
}
|
|
196
|
+
//#endregion
|
|
197
|
+
//#region src/local/protocol.d.ts
|
|
198
|
+
/**
|
|
199
|
+
* Wire tag on every message, so one `LocalChannelPort` can carry this
|
|
200
|
+
* request/response protocol without being confused with anything else on the
|
|
201
|
+
* port, and so a version skew is a readable refusal rather than a crash.
|
|
202
|
+
*/
|
|
203
|
+
export declare const LOCAL_ATTACHMENT_PROTOCOL = "ph-attachment/v1";
|
|
204
|
+
/** Default bytes per `chunk` message. */
|
|
205
|
+
export declare const DEFAULT_LOCAL_CHUNK_BYTES: number;
|
|
206
|
+
/** Default wait for the next message of a request before it is abandoned. */
|
|
207
|
+
export declare const DEFAULT_LOCAL_REQUEST_TIMEOUT_MS = 30000;
|
|
208
|
+
/** Ask a peer for the bytes of `hash`, authorized by `documentId`. */
|
|
209
|
+
type LocalAttachmentFetchRequest = {
|
|
210
|
+
protocol: typeof LOCAL_ATTACHMENT_PROTOCOL;
|
|
211
|
+
kind: "fetch";
|
|
212
|
+
/** Unique per requesting instance; the peer echoes it on every reply. */
|
|
213
|
+
id: string;
|
|
214
|
+
hash: AttachmentHash;
|
|
215
|
+
documentId: string;
|
|
216
|
+
};
|
|
217
|
+
/** Abandon an in-flight `fetch` so the peer stops reading and sending. */
|
|
218
|
+
type LocalAttachmentCancelRequest = {
|
|
219
|
+
protocol: typeof LOCAL_ATTACHMENT_PROTOCOL;
|
|
220
|
+
kind: "cancel";
|
|
221
|
+
id: string;
|
|
222
|
+
};
|
|
223
|
+
type LocalAttachmentRequest = LocalAttachmentFetchRequest | LocalAttachmentCancelRequest;
|
|
224
|
+
/**
|
|
225
|
+
* The peer holds the bytes and is about to stream them. `metadata` arrives up
|
|
226
|
+
* front so the requester can start a body stream before the first byte.
|
|
227
|
+
*/
|
|
228
|
+
type LocalAttachmentBeginResponse = {
|
|
229
|
+
protocol: typeof LOCAL_ATTACHMENT_PROTOCOL;
|
|
230
|
+
kind: "begin";
|
|
231
|
+
id: string;
|
|
232
|
+
hash: AttachmentHash;
|
|
233
|
+
metadata: AttachmentMetadata;
|
|
234
|
+
};
|
|
235
|
+
/**
|
|
236
|
+
* One slice of the body. `bytes` is a `Uint8Array`, which both a browser
|
|
237
|
+
* `MessagePort` and a `node:worker_threads` one structured-clone natively.
|
|
238
|
+
* `seq` lets a receiver assert ordering rather than assume it.
|
|
239
|
+
*/
|
|
240
|
+
type LocalAttachmentChunkResponse = {
|
|
241
|
+
protocol: typeof LOCAL_ATTACHMENT_PROTOCOL;
|
|
242
|
+
kind: "chunk";
|
|
243
|
+
id: string;
|
|
244
|
+
seq: number;
|
|
245
|
+
bytes: Uint8Array;
|
|
246
|
+
};
|
|
247
|
+
/** The body is complete. */
|
|
248
|
+
type LocalAttachmentEndResponse = {
|
|
249
|
+
protocol: typeof LOCAL_ATTACHMENT_PROTOCOL;
|
|
250
|
+
kind: "end";
|
|
251
|
+
id: string;
|
|
252
|
+
};
|
|
253
|
+
/** Retry later: the peer holds a reservation without bytes, or is at its serve cap. */
|
|
254
|
+
type LocalAttachmentPendingResponse = {
|
|
255
|
+
protocol: typeof LOCAL_ATTACHMENT_PROTOCOL;
|
|
256
|
+
kind: "pending";
|
|
257
|
+
id: string;
|
|
258
|
+
hash: AttachmentHash;
|
|
259
|
+
expiresAtUtc: string;
|
|
260
|
+
retryAfterMs: number;
|
|
261
|
+
/** At its serve cap: the peer did not authorize or look up the hash. */
|
|
262
|
+
busy?: true;
|
|
263
|
+
};
|
|
264
|
+
/**
|
|
265
|
+
* The peer will not serve the hash: it does not hold the bytes, or the
|
|
266
|
+
* document does not authorize the read.
|
|
267
|
+
*
|
|
268
|
+
* The two are deliberately one answer. Distinguishing them would tell a peer
|
|
269
|
+
* which hashes exist without authorization, and the requester's handling is
|
|
270
|
+
* the same either way -- a bounded retry, because an unauthorized answer is
|
|
271
|
+
* most often the serving reactor's attachment reference index lagging its own
|
|
272
|
+
* sync (see `AttachmentRetryPolicy.notFoundAttempts`).
|
|
273
|
+
*/
|
|
274
|
+
type LocalAttachmentNotFoundResponse = {
|
|
275
|
+
protocol: typeof LOCAL_ATTACHMENT_PROTOCOL;
|
|
276
|
+
kind: "not-found";
|
|
277
|
+
id: string;
|
|
278
|
+
};
|
|
279
|
+
/** The peer failed while serving; the requester treats this as a retryable error. */
|
|
280
|
+
type LocalAttachmentErrorResponse = {
|
|
281
|
+
protocol: typeof LOCAL_ATTACHMENT_PROTOCOL;
|
|
282
|
+
kind: "error";
|
|
283
|
+
id: string;
|
|
284
|
+
message: string;
|
|
285
|
+
};
|
|
286
|
+
type LocalAttachmentResponse = LocalAttachmentBeginResponse | LocalAttachmentChunkResponse | LocalAttachmentEndResponse | LocalAttachmentPendingResponse | LocalAttachmentNotFoundResponse | LocalAttachmentErrorResponse;
|
|
287
|
+
type LocalAttachmentMessage = LocalAttachmentRequest | LocalAttachmentResponse;
|
|
288
|
+
/**
|
|
289
|
+
* Whether `value` is a request for this protocol.
|
|
290
|
+
*
|
|
291
|
+
* Both halves live on one port -- each peer runs a transport AND a server over
|
|
292
|
+
* the same `LocalChannelPort` -- so each half has to recognise only its own
|
|
293
|
+
* side of the conversation and ignore the rest rather than treat it as
|
|
294
|
+
* malformed.
|
|
295
|
+
*/
|
|
296
|
+
export declare function isLocalAttachmentRequest(value: unknown): value is LocalAttachmentRequest;
|
|
297
|
+
/** Whether `value` is a response for this protocol. */
|
|
298
|
+
export declare function isLocalAttachmentResponse(value: unknown): value is LocalAttachmentResponse;
|
|
299
|
+
//#endregion
|
|
300
|
+
//#region src/peers/peered-attachment-transport.d.ts
|
|
301
|
+
type PeeredAttachmentTransportOptions = {
|
|
302
|
+
/** Read at fetch time for the Switchboard origins of its gql remotes. */
|
|
303
|
+
syncManager?: Pick<ISyncManager, "list">;
|
|
304
|
+
/** An attachment host that is not one of the sync remotes. */
|
|
305
|
+
switchboardUrl?: string;
|
|
306
|
+
jwtHandler?: JwtHandler;
|
|
307
|
+
fetchFn?: typeof fetch;
|
|
308
|
+
};
|
|
309
|
+
/**
|
|
310
|
+
* Peers in parallel, then Switchboards in turn; verified data > pending >
|
|
311
|
+
* busy > error > unanimous not-found.
|
|
312
|
+
*/
|
|
313
|
+
export declare class PeeredAttachmentTransport implements IAttachmentTransport {
|
|
314
|
+
private readonly syncManager;
|
|
315
|
+
private readonly switchboardUrl;
|
|
316
|
+
private readonly jwtHandler;
|
|
317
|
+
private readonly fetchFn;
|
|
318
|
+
private readonly peers;
|
|
319
|
+
private readonly switchboards;
|
|
320
|
+
constructor(options?: PeeredAttachmentTransportOptions);
|
|
321
|
+
/** Keyed by (peerId, channelName), so one peer may hold several links. */
|
|
322
|
+
addPeer(peerId: string, channelName: string, transport: IAttachmentTransport): void;
|
|
323
|
+
/** Forgets one peer link; the caller owns closing its transport. */
|
|
324
|
+
removePeer(peerId: string, channelName: string): IAttachmentTransport | undefined;
|
|
325
|
+
/** Distinct linked peer ids. */
|
|
326
|
+
peerNames(): readonly string[];
|
|
327
|
+
/** The explicit Switchboard, then every gql remote's origin. */
|
|
328
|
+
switchboardSources(): readonly string[];
|
|
329
|
+
fetch(hash: AttachmentHash, documentId: string, signal?: AbortSignal): Promise<TransportFetchResult>;
|
|
330
|
+
private ask;
|
|
331
|
+
/** The first verified data wins and cancels the other peers. */
|
|
332
|
+
private askPeers;
|
|
333
|
+
/** Best effort across every source. */
|
|
334
|
+
announce(hash: AttachmentHash): Promise<void>;
|
|
335
|
+
push(): Promise<void>;
|
|
336
|
+
private sources;
|
|
337
|
+
private switchboardFor;
|
|
338
|
+
}
|
|
339
|
+
/** Attachments are served from the origin, not under the GraphQL path. */
|
|
340
|
+
export declare function attachmentOriginOf(graphqlUrl: string): string;
|
|
341
|
+
//#endregion
|
|
342
|
+
//#region src/peers/attachment-peer-links.d.ts
|
|
343
|
+
type AttachmentServedStats = {
|
|
344
|
+
served: number;
|
|
345
|
+
bytesServed: number;
|
|
346
|
+
refused: number;
|
|
347
|
+
};
|
|
348
|
+
type AttachmentPeerLinksOptions = {
|
|
349
|
+
/** What linked peers are served from. */
|
|
350
|
+
store: IAttachmentStore;
|
|
351
|
+
/** Where each link's pulling half is registered. */
|
|
352
|
+
transport: PeeredAttachmentTransport;
|
|
353
|
+
/** Absent, and absent on the link, every read is refused. */
|
|
354
|
+
authorize?: LocalAttachmentAuthorizer;
|
|
355
|
+
chunkSizeBytes?: number;
|
|
356
|
+
requestTimeoutMs?: number;
|
|
357
|
+
/** Each link's transport refuses a body declared larger than this. */
|
|
358
|
+
maxBytes?: number;
|
|
359
|
+
/** Requests each link's server serves at once. */
|
|
360
|
+
maxConcurrentServes?: number;
|
|
361
|
+
/** Called after a link is added; a host re-chases terminal hashes here. */
|
|
362
|
+
onPeerAdded?: (peerId: string, channelName: string) => void;
|
|
363
|
+
onDiagnostic?: (message: string, error?: unknown) => void;
|
|
364
|
+
};
|
|
365
|
+
type AttachmentPeerLinkOptions = {
|
|
366
|
+
/** Replaces the links-wide authorizer for this link. */
|
|
367
|
+
authorize?: LocalAttachmentAuthorizer;
|
|
368
|
+
};
|
|
369
|
+
/** Per-peer server and transport pairs, each on its own port, not the sync port. */
|
|
370
|
+
export declare class AttachmentPeerLinks {
|
|
371
|
+
private readonly options;
|
|
372
|
+
private readonly links;
|
|
373
|
+
private readonly onDiagnostic;
|
|
374
|
+
constructor(options: AttachmentPeerLinksOptions);
|
|
375
|
+
has(peerId: string, channelName: string): boolean;
|
|
376
|
+
/** Takes ownership of `port` on success; on failure the port is untouched. */
|
|
377
|
+
addPeer(peerId: string, channelName: string, port: LocalChannelPort, linkOptions?: AttachmentPeerLinkOptions): void;
|
|
378
|
+
/** Closes the link's server, transport and port. False when absent. */
|
|
379
|
+
removePeer(peerId: string, channelName: string): boolean;
|
|
380
|
+
servedStats(): AttachmentServedStats;
|
|
381
|
+
close(): void;
|
|
382
|
+
}
|
|
383
|
+
//#endregion
|
|
384
|
+
//#region src/replication/types.d.ts
|
|
385
|
+
/**
|
|
386
|
+
* Pulls the attachment refs an operation carries.
|
|
387
|
+
*
|
|
388
|
+
* The replicator depends on this rather than on the schema compiler directly
|
|
389
|
+
* so a host that has no compiled document models (a test, a reactor whose
|
|
390
|
+
* models declare no `AttachmentRef` field but carry refs by another
|
|
391
|
+
* convention) can still feed it. The production implementation is
|
|
392
|
+
* `SchemaCompiledOperationRefs`, which is the same compiler + registry pair
|
|
393
|
+
* `AttachmentReferenceReadModel` uses, so the refs the replicator chases and
|
|
394
|
+
* the refs the reference index records are extracted by one piece of code.
|
|
395
|
+
*/
|
|
396
|
+
interface IOperationAttachmentRefs {
|
|
397
|
+
refsOf(item: OperationWithContext): readonly AttachmentRef[];
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* What the replicator is doing about one hash.
|
|
401
|
+
*
|
|
402
|
+
* - `queued`: known missing locally, waiting for a concurrency slot.
|
|
403
|
+
* - `fetching`: a transport fetch is in flight.
|
|
404
|
+
* - `held`: the bytes are in the local store. Terminal and the goal; a held
|
|
405
|
+
* hash leaves the report.
|
|
406
|
+
* - `waiting`: the last answer was an unexpired `pending`, a bounded
|
|
407
|
+
* `not-found` (see {@link AttachmentRetryPolicy.notFoundAttempts}), or a
|
|
408
|
+
* transport error; a retry is scheduled.
|
|
409
|
+
* - `not-found`: the `not-found` budget is spent (an expired `pending` counts
|
|
410
|
+
* as a `not-found`), no document is left unasked and no `pending` is live.
|
|
411
|
+
* - `failed`: the transport kept erroring and no document is left unasked.
|
|
412
|
+
*
|
|
413
|
+
* Both terminal states wait for `retry()`, or for a document not yet seen to
|
|
414
|
+
* reference the hash, which earns one more attempt through that document.
|
|
415
|
+
*/
|
|
416
|
+
type AttachmentReplicationState = "queued" | "fetching" | "held" | "waiting" | "not-found" | "failed";
|
|
417
|
+
/**
|
|
418
|
+
* Retry shape for the three answers a fetch can give, honouring the
|
|
419
|
+
* `pending | not-found | data` triad that
|
|
420
|
+
* {@link IAttachmentTransport.fetch} exists to express.
|
|
421
|
+
*
|
|
422
|
+
* `notFoundAttempts` is the reference-index race budget and the reason
|
|
423
|
+
* `not-found` is retried at all: a peer authorizes a byte fetch through its
|
|
424
|
+
* OWN attachment reference index, and that index lags that peer's sync. So the
|
|
425
|
+
* first `not-found` for a freshly synced ref is far more likely to mean "that
|
|
426
|
+
* peer has not indexed this operation yet" than "these bytes do not exist".
|
|
427
|
+
* Retrying it a bounded number of times covers the lag without turning a
|
|
428
|
+
* genuinely missing hash into an unbounded loop -- and the counts keep both
|
|
429
|
+
* outcomes visible rather than papering over either.
|
|
430
|
+
*/
|
|
431
|
+
type AttachmentRetryPolicy = {
|
|
432
|
+
/** Fallback wait when a `pending` answer carries no `retryAfterMs`. */
|
|
433
|
+
pendingRetryMs: number;
|
|
434
|
+
/** Shortest wait honoured after a `pending`, whatever it asks for. */
|
|
435
|
+
minPendingRetryMs: number;
|
|
436
|
+
/** Cap for a run of `pending` answers, whose wait doubles per answer. */
|
|
437
|
+
maxPendingRetryMs: number;
|
|
438
|
+
/** `not-found` answers absorbed as index lag; cumulative until `retry()`. */
|
|
439
|
+
notFoundAttempts: number;
|
|
440
|
+
/** Base wait between `not-found` retries; doubles per attempt. */
|
|
441
|
+
notFoundRetryMs: number;
|
|
442
|
+
/** Transport errors in a row before giving up; any answer ends the run. */
|
|
443
|
+
errorAttempts: number;
|
|
444
|
+
/** Base wait between error retries; doubles per attempt. */
|
|
445
|
+
errorRetryMs: number;
|
|
446
|
+
/** Cap for not-found and error waits. */
|
|
447
|
+
maxRetryMs: number;
|
|
448
|
+
};
|
|
449
|
+
export declare const DEFAULT_ATTACHMENT_RETRY_POLICY: AttachmentRetryPolicy;
|
|
450
|
+
/** Default bounded concurrency for in-flight attachment fetches. */
|
|
451
|
+
export declare const DEFAULT_ATTACHMENT_REPLICATION_CONCURRENCY = 3;
|
|
452
|
+
/** Default number of held hashes a replicator remembers after dropping their entries. */
|
|
453
|
+
export declare const DEFAULT_ATTACHMENT_HELD_HASH_LIMIT = 10000;
|
|
454
|
+
/** Default page size for the boot re-scan over the reference backlog. */
|
|
455
|
+
export declare const DEFAULT_ATTACHMENT_BACKLOG_PAGE_SIZE = 200;
|
|
456
|
+
/**
|
|
457
|
+
* The replicator's observable state: what it has seen, what it holds, and what
|
|
458
|
+
* it is still arguing about.
|
|
459
|
+
*
|
|
460
|
+
* Reported rather than inferred, because the two honest failure modes --
|
|
461
|
+
* a peer that never had the bytes, and a peer whose reference index has not
|
|
462
|
+
* caught up -- are indistinguishable from the outside at the moment they
|
|
463
|
+
* happen and only separable over time. `notFound` and `waiting` are both
|
|
464
|
+
* surfaced so an operator sees which one a reactor is in.
|
|
465
|
+
*/
|
|
466
|
+
type AttachmentReplicatorStatus = {
|
|
467
|
+
/** Whether the replicator is subscribed and scheduling. */
|
|
468
|
+
running: boolean;
|
|
469
|
+
/** Distinct hashes tracked: every entry plus the remembered held hashes. */
|
|
470
|
+
refsSeen: number;
|
|
471
|
+
/** Remembered held hashes, at most `heldHashLimit`. */
|
|
472
|
+
held: number;
|
|
473
|
+
/** `storageUsed()` of the local store, in bytes. */
|
|
474
|
+
bytesHeld: number;
|
|
475
|
+
/** Hashes waiting for a concurrency slot. */
|
|
476
|
+
queued: number;
|
|
477
|
+
/** Hashes with a fetch in flight. */
|
|
478
|
+
fetching: number;
|
|
479
|
+
/** Hashes with a retry scheduled (pending, lagging index, or error). */
|
|
480
|
+
waiting: number;
|
|
481
|
+
/** Hashes the peer answered `not-found` for, past the lag budget. */
|
|
482
|
+
notFound: number;
|
|
483
|
+
/** Hashes whose transport kept erroring. */
|
|
484
|
+
failed: number;
|
|
485
|
+
/** Whether the boot re-scan over the reference backlog has completed. */
|
|
486
|
+
backlogScanned: boolean;
|
|
487
|
+
/** The most recent transport error message, for the inspector. */
|
|
488
|
+
lastError: string | undefined;
|
|
489
|
+
};
|
|
490
|
+
/** One hash's replication record, as reported for diagnostics. */
|
|
491
|
+
type AttachmentReplicationEntry = {
|
|
492
|
+
hash: AttachmentHash;
|
|
493
|
+
state: AttachmentReplicationState;
|
|
494
|
+
/** Documents whose operations referenced the hash; any of them may authorize. */
|
|
495
|
+
documentIds: readonly string[];
|
|
496
|
+
attempts: number;
|
|
497
|
+
notFoundAnswers: number;
|
|
498
|
+
/** Epoch ms of the next scheduled attempt, when one is scheduled. */
|
|
499
|
+
nextAttemptAtMs: number | undefined;
|
|
500
|
+
lastError: string | undefined;
|
|
501
|
+
};
|
|
502
|
+
//#endregion
|
|
503
|
+
//#region src/replication/attachment-replicator.d.ts
|
|
504
|
+
/**
|
|
505
|
+
* Clock and timer seam. Injected rather than taken from globals so a test can
|
|
506
|
+
* drive every retry deadline deterministically, without fake timers racing the
|
|
507
|
+
* replicator's own promises.
|
|
508
|
+
*/
|
|
509
|
+
type ReplicationTimers = {
|
|
510
|
+
/** Epoch ms; a pending answer's `expiresAtUtc` is compared against it. */
|
|
511
|
+
now: () => number;
|
|
512
|
+
setTimer: (callback: () => void, delayMs: number) => unknown;
|
|
513
|
+
clearTimer: (handle: unknown) => void;
|
|
514
|
+
};
|
|
515
|
+
export declare const SYSTEM_REPLICATION_TIMERS: ReplicationTimers;
|
|
516
|
+
type AttachmentReplicatorOptions = {
|
|
517
|
+
/** Where bytes land, and the authority on what is already held. */
|
|
518
|
+
store: IAttachmentStore;
|
|
519
|
+
/** Where bytes come from. */
|
|
520
|
+
transport: IAttachmentTransport;
|
|
521
|
+
/** Pulls refs out of a committed operation. */
|
|
522
|
+
refs: IOperationAttachmentRefs;
|
|
523
|
+
/** The reactor's bus; the replicator subscribes to `JOB_READ_READY`. */
|
|
524
|
+
eventBus: IEventBus;
|
|
525
|
+
/**
|
|
526
|
+
* The durable reference list re-scanned on {@link AttachmentReplicator.start}.
|
|
527
|
+
* Omitted, the replicator only ever learns about refs from live operations --
|
|
528
|
+
* correct for a reactor with no reference index, and NOT resumable across a
|
|
529
|
+
* restart, which {@link AttachmentReplicatorStatus.backlogScanned} reports.
|
|
530
|
+
*/
|
|
531
|
+
backlog?: IAttachmentReferenceScanner;
|
|
532
|
+
concurrency?: number;
|
|
533
|
+
retry?: Partial<AttachmentRetryPolicy>;
|
|
534
|
+
backlogPageSize?: number;
|
|
535
|
+
/**
|
|
536
|
+
* Held hashes remembered after their entry is dropped, oldest evicted first.
|
|
537
|
+
* An evicted hash referenced again costs one `store.has()`, never a fetch.
|
|
538
|
+
*/
|
|
539
|
+
heldHashLimit?: number;
|
|
540
|
+
/**
|
|
541
|
+
* Whether fetched bytes are hashed and checked against the hash they were
|
|
542
|
+
* requested under. Defaults to true: a local peer transport means the bytes
|
|
543
|
+
* come from another reactor rather than from a trusted server, and a
|
|
544
|
+
* content-addressed store that accepts bytes it never verified is no longer
|
|
545
|
+
* content-addressed.
|
|
546
|
+
*/
|
|
547
|
+
verifyHash?: boolean;
|
|
548
|
+
timers?: ReplicationTimers;
|
|
549
|
+
onDiagnostic?: (message: string, error?: unknown) => void;
|
|
550
|
+
};
|
|
551
|
+
/**
|
|
552
|
+
* Lazy fetch-on-reference: pulls attachment bytes a reactor's own operations
|
|
553
|
+
* reference but whose hash its local store lacks.
|
|
554
|
+
*
|
|
555
|
+
* The seam is `ReactorEventTypes.JOB_READ_READY` on the reactor's event bus,
|
|
556
|
+
* and that choice matters in two ways. It is AFTER the pre-ready read models,
|
|
557
|
+
* so by the time the replicator asks a peer for a hash, this reactor's own
|
|
558
|
+
* attachment reference index has recorded the reference -- the same reactor is
|
|
559
|
+
* therefore able to serve the hash onward, and a reactor cannot be in the
|
|
560
|
+
* position of chasing bytes it would refuse to authorize itself. And it is off
|
|
561
|
+
* the write path: a slow or unreachable peer cannot delay a commit, because
|
|
562
|
+
* the subscriber only notes hashes and returns.
|
|
563
|
+
*
|
|
564
|
+
* Resume is a re-scan of the reference index ({@link IAttachmentReferenceScanner}),
|
|
565
|
+
* not a cursor: the index plus `store.has()` re-derives the exact outstanding
|
|
566
|
+
* work set on every boot, idempotently, and a persisted cursor could only add a
|
|
567
|
+
* second truth that is wrong in the one direction that loses data.
|
|
568
|
+
*
|
|
569
|
+
* Loop-safety is structural: one entry per hash, created once, and every
|
|
570
|
+
* retry decision is {@link nextAfter}'s. A terminal entry (`not-found`,
|
|
571
|
+
* `failed`) is never re-queued by a further reference from a document it
|
|
572
|
+
* already knows; a document it has not seen earns it one more attempt, through
|
|
573
|
+
* that document. Otherwise only {@link AttachmentReplicator.retry} moves a
|
|
574
|
+
* terminal entry back. A held hash drops its entry and is remembered in a
|
|
575
|
+
* bounded set, so the map holds only unfinished work.
|
|
576
|
+
*/
|
|
577
|
+
export declare class AttachmentReplicator {
|
|
578
|
+
private readonly store;
|
|
579
|
+
private readonly transport;
|
|
580
|
+
private readonly refs;
|
|
581
|
+
private readonly eventBus;
|
|
582
|
+
private readonly backlog;
|
|
583
|
+
private readonly concurrency;
|
|
584
|
+
private readonly policy;
|
|
585
|
+
private readonly backlogPageSize;
|
|
586
|
+
private readonly heldHashLimit;
|
|
587
|
+
private readonly verifyHash;
|
|
588
|
+
private readonly timers;
|
|
589
|
+
private readonly onDiagnostic;
|
|
590
|
+
private readonly entries;
|
|
591
|
+
private readonly heldHashes;
|
|
592
|
+
private readonly queue;
|
|
593
|
+
private readonly idleWaiters;
|
|
594
|
+
private readonly aborts;
|
|
595
|
+
private unsubscribe;
|
|
596
|
+
private running;
|
|
597
|
+
private inFlight;
|
|
598
|
+
private timerHandle;
|
|
599
|
+
private timerAtMs;
|
|
600
|
+
private backlogScan;
|
|
601
|
+
private backlogDone;
|
|
602
|
+
private lastError;
|
|
603
|
+
constructor(options: AttachmentReplicatorOptions);
|
|
604
|
+
/**
|
|
605
|
+
* Subscribes to committed operations and starts the boot re-scan.
|
|
606
|
+
*
|
|
607
|
+
* The subscription is taken BEFORE the scan so no operation committed during
|
|
608
|
+
* the scan can slip between the two. A hash both paths find is one entry.
|
|
609
|
+
*
|
|
610
|
+
* A restart resumes its own unfinished work: a `stop()` empties the queue but
|
|
611
|
+
* keeps the entries, so entries left `queued` or `waiting` are re-armed here.
|
|
612
|
+
* Without that, a hash that was outstanding at `stop()` would sit in the map
|
|
613
|
+
* forever unless an unrelated live operation happened to pump the queue --
|
|
614
|
+
* the backlog re-scan does not rescue it, because `observeReference` treats
|
|
615
|
+
* an already-known hash as nothing to do.
|
|
616
|
+
*/
|
|
617
|
+
start(): void;
|
|
618
|
+
/**
|
|
619
|
+
* Stops scheduling, drops the subscription and aborts in-flight fetches.
|
|
620
|
+
*
|
|
621
|
+
* Entries are kept: the in-memory retry counters are not durable state and a
|
|
622
|
+
* restart legitimately starts the budgets over (a hash that was `not-found`
|
|
623
|
+
* because a peer's index lagged deserves another look after a reboot), but a
|
|
624
|
+
* `stop()` that is not a restart should not forget what is held either --
|
|
625
|
+
* `store.has()` is re-checked before every fetch regardless.
|
|
626
|
+
*/
|
|
627
|
+
stop(): Promise<void>;
|
|
628
|
+
/** Notes every ref carried by `items`. Synchronous: it only enqueues. */
|
|
629
|
+
observeOperations(items: readonly OperationWithContext[]): void;
|
|
630
|
+
/** Notes one (document, ref) pair, queueing the hash if it is new. */
|
|
631
|
+
observeReference(documentId: string, ref: AttachmentRef): void;
|
|
632
|
+
status(): Promise<AttachmentReplicatorStatus>;
|
|
633
|
+
/** Every hash not yet held and what is happening to it. */
|
|
634
|
+
report(): AttachmentReplicationEntry[];
|
|
635
|
+
/**
|
|
636
|
+
* Re-queues terminal entries so a `not-found` or `failed` hash is chased
|
|
637
|
+
* again, resetting their budgets. `hash` narrows it to one; absent, every
|
|
638
|
+
* terminal entry is retried.
|
|
639
|
+
*
|
|
640
|
+
* The deliberate manual lever over the reference-index race: when an
|
|
641
|
+
* operator can see that a peer has caught up, nothing should make them wait
|
|
642
|
+
* for a reboot to find out.
|
|
643
|
+
*/
|
|
644
|
+
retry(hash?: AttachmentHash): void;
|
|
645
|
+
/**
|
|
646
|
+
* Resolves when nothing is queued or in flight.
|
|
647
|
+
*
|
|
648
|
+
* Entries `waiting` on a retry deadline do NOT hold this open -- their next
|
|
649
|
+
* attempt is the timer's business, and a caller that wants to reach it
|
|
650
|
+
* advances the injected clock. A test seam, and what a load harness waits on.
|
|
651
|
+
*/
|
|
652
|
+
idle(): Promise<void>;
|
|
653
|
+
/** Awaits the boot re-scan; resolves immediately when there is no backlog. */
|
|
654
|
+
backlogScanned(): Promise<void>;
|
|
655
|
+
/** Re-queues every non-terminal entry a previous `stop()` stranded. */
|
|
656
|
+
private resumeOutstanding;
|
|
657
|
+
private scanBacklog;
|
|
658
|
+
/** Starts as many queued fetches as the concurrency bound allows. */
|
|
659
|
+
private pump;
|
|
660
|
+
private fetchOne;
|
|
661
|
+
private attempt;
|
|
662
|
+
private apply;
|
|
663
|
+
private markHeld;
|
|
664
|
+
private recordError;
|
|
665
|
+
private schedule;
|
|
666
|
+
/** Keeps exactly one timer, set to the earliest outstanding deadline. */
|
|
667
|
+
private armTimer;
|
|
668
|
+
private onTimer;
|
|
669
|
+
private cancelTimer;
|
|
670
|
+
private settleIdle;
|
|
671
|
+
}
|
|
672
|
+
/** A scanner over a plain list, for a host without a reference index. */
|
|
673
|
+
export declare function staticAttachmentReferenceScanner(references: readonly AttachmentReferenceRow[]): IAttachmentReferenceScanner;
|
|
674
|
+
//#endregion
|
|
675
|
+
//#region src/replication/hash.d.ts
|
|
676
|
+
/**
|
|
677
|
+
* Lowercase SHA-256 hex of `bytes` -- the `attachment://v1:` hash format
|
|
678
|
+
* (`packages/reactor/src/attachments/types.ts`).
|
|
679
|
+
*
|
|
680
|
+
* WebCrypto, so the same code runs in Node and in a browser realm. There is no
|
|
681
|
+
* streaming digest in WebCrypto, which is why verification happens over
|
|
682
|
+
* collected bytes.
|
|
683
|
+
*/
|
|
684
|
+
export declare function sha256Hex(bytes: Uint8Array): Promise<AttachmentHash>;
|
|
685
|
+
//#endregion
|
|
686
|
+
//#region src/replication/operation-attachment-refs.d.ts
|
|
687
|
+
/**
|
|
688
|
+
* The production {@link IOperationAttachmentRefs}: the compiled, schema-driven
|
|
689
|
+
* extractor, reused rather than reimplemented.
|
|
690
|
+
*
|
|
691
|
+
* This is the same `(registry, compiler)` pair `AttachmentReferenceReadModel`
|
|
692
|
+
* runs, so a ref the index records and a ref the replicator chases are found by
|
|
693
|
+
* one piece of code -- including the compiler's per-module/per-action cache,
|
|
694
|
+
* which is held by the compiler instance a host shares between the two. A host
|
|
695
|
+
* with no read model to share one with takes the default compiler.
|
|
696
|
+
*
|
|
697
|
+
* Extraction failures do NOT propagate. A malformed ref or an unregistered
|
|
698
|
+
* document type must not take down the event-bus subscriber the replicator
|
|
699
|
+
* runs on: that would turn an attachment problem into a reactor-wide write
|
|
700
|
+
* failure, since `IEventBus.emit` aggregates subscriber errors back to the
|
|
701
|
+
* emitter. The read model is the component whose job is to refuse a bad ref
|
|
702
|
+
* loudly; this one is a best-effort byte chaser, so it reports and moves on.
|
|
703
|
+
*/
|
|
704
|
+
export declare class SchemaCompiledOperationRefs implements IOperationAttachmentRefs {
|
|
705
|
+
private readonly registry;
|
|
706
|
+
private readonly compiler;
|
|
707
|
+
private readonly onDiagnostic;
|
|
708
|
+
constructor(registry: IDocumentModelRegistry, compiler?: IAttachmentSchemaCompiler, onDiagnostic?: (message: string, error: unknown) => void);
|
|
709
|
+
refsOf(item: OperationWithContext): readonly AttachmentRef[];
|
|
710
|
+
}
|
|
711
|
+
//#endregion
|
|
712
|
+
//#region src/storage/local/bytes.d.ts
|
|
713
|
+
/**
|
|
714
|
+
* Drains `stream` into one `Uint8Array`.
|
|
715
|
+
*
|
|
716
|
+
* The local stores hold whole blobs (see {@link ILocalAttachmentBackend}), so
|
|
717
|
+
* the stream an `IAttachmentStore.put` is handed has to be collected before it
|
|
718
|
+
* can be written. Realm-neutral: no `node:buffer`, no `Blob`.
|
|
719
|
+
*/
|
|
720
|
+
export declare function collectStream(stream: ReadableStream<Uint8Array>): Promise<Uint8Array>;
|
|
721
|
+
/** A one-chunk `ReadableStream` over `bytes`. */
|
|
722
|
+
export declare function streamFromBytes(bytes: Uint8Array): ReadableStream<Uint8Array>;
|
|
723
|
+
//#endregion
|
|
724
|
+
//#region src/storage/local/types.d.ts
|
|
725
|
+
/**
|
|
726
|
+
* One attachment's metadata row in a local, realm-resident store.
|
|
727
|
+
*
|
|
728
|
+
* The committed subset of {@link AttachmentHeader}: no `pending` status and no
|
|
729
|
+
* `expiresAtUtc`, because a local store of this kind holds no reservation
|
|
730
|
+
* table. Pending is a property of an in-flight UPLOAD, which belongs to the
|
|
731
|
+
* server-side `KyselyAttachmentStore`/`IReservationStore` pair; a replicating
|
|
732
|
+
* store only ever learns "pending" from a transport answer and never persists
|
|
733
|
+
* it.
|
|
734
|
+
*/
|
|
735
|
+
type LocalAttachmentRecord = {
|
|
736
|
+
hash: AttachmentHash;
|
|
737
|
+
mimeType: string;
|
|
738
|
+
fileName: string;
|
|
739
|
+
/**
|
|
740
|
+
* The byte length actually held (or last held, for an evicted record), not
|
|
741
|
+
* the producer's claim. `AttachmentMetadata.sizeBytes` arrives over a wire
|
|
742
|
+
* and is only as honest as its sender; `storageUsed()` sums this field, so
|
|
743
|
+
* it is measured from the bytes received.
|
|
744
|
+
*/
|
|
745
|
+
sizeBytes: number;
|
|
746
|
+
extension: string | null;
|
|
747
|
+
status: "available" | "evicted";
|
|
748
|
+
source: "local" | "sync";
|
|
749
|
+
createdAtUtc: string;
|
|
750
|
+
lastAccessedAtUtc: string;
|
|
751
|
+
};
|
|
752
|
+
/**
|
|
753
|
+
* The persistence seam under {@link LocalAttachmentStore}: a content-addressed
|
|
754
|
+
* record store plus a blob store, and nothing else.
|
|
755
|
+
*
|
|
756
|
+
* Every `IAttachmentStore` semantic -- dedup on put, evicted-then-restored,
|
|
757
|
+
* access-time bumping, transport re-fetch, what `has()` means -- lives in
|
|
758
|
+
* {@link LocalAttachmentStore} above this interface, so the two backends
|
|
759
|
+
* (IndexedDB for a browser, a memory twin for Node) cannot drift on behaviour.
|
|
760
|
+
* A backend only has to move bytes and rows.
|
|
761
|
+
*
|
|
762
|
+
* Bytes cross this interface as whole `Uint8Array`s rather than streams on
|
|
763
|
+
* purpose: IndexedDB has no streaming value API, so a chunked backend would
|
|
764
|
+
* have to reassemble anyway, and a browser-side attachment store is bounded by
|
|
765
|
+
* what the realm can hold regardless.
|
|
766
|
+
*/
|
|
767
|
+
interface ILocalAttachmentBackend {
|
|
768
|
+
/** The record for `hash`, or undefined when the hash is unknown. */
|
|
769
|
+
readRecord(hash: AttachmentHash): Promise<LocalAttachmentRecord | undefined>;
|
|
770
|
+
/**
|
|
771
|
+
* Writes `record` and `bytes` as ONE unit, replacing whatever was there.
|
|
772
|
+
*
|
|
773
|
+
* Atomicity matters: a record claiming `available` with no blob behind it
|
|
774
|
+
* reads as a store that holds bytes it cannot serve, which is exactly the
|
|
775
|
+
* state the replicator would never retry.
|
|
776
|
+
*/
|
|
777
|
+
write(record: LocalAttachmentRecord, bytes: Uint8Array): Promise<void>;
|
|
778
|
+
/** The bytes for `hash`, or undefined when none are held. */
|
|
779
|
+
readBytes(hash: AttachmentHash): Promise<Uint8Array | undefined>;
|
|
780
|
+
/**
|
|
781
|
+
* Writes `record` (expected to carry `status: "evicted"`) and deletes its
|
|
782
|
+
* blob as one unit. The record is retained so the hash stays known.
|
|
783
|
+
*/
|
|
784
|
+
evict(record: LocalAttachmentRecord): Promise<void>;
|
|
785
|
+
/** Sets `lastAccessedAtUtc` on an existing record; a no-op for an unknown hash. */
|
|
786
|
+
touch(hash: AttachmentHash, lastAccessedAtUtc: string): Promise<void>;
|
|
787
|
+
/** Summed `sizeBytes` of every record whose status is `available`. */
|
|
788
|
+
availableBytes(): Promise<number>;
|
|
789
|
+
/** Releases the underlying handle. Idempotent. */
|
|
790
|
+
close(): Promise<void>;
|
|
791
|
+
}
|
|
792
|
+
//#endregion
|
|
793
|
+
//#region src/storage/local/idb-attachment-backend.d.ts
|
|
794
|
+
/** Metadata rows, keyed by content hash. */
|
|
795
|
+
export declare const IDB_RECORD_STORE = "records";
|
|
796
|
+
/** Blob rows (`{ hash, bytes }`), keyed by the same content hash. */
|
|
797
|
+
export declare const IDB_BLOB_STORE = "blobs";
|
|
798
|
+
/** Index over {@link LocalAttachmentRecord.status}; what `storageUsed()` scans. */
|
|
799
|
+
export declare const IDB_STATUS_INDEX = "by_status";
|
|
800
|
+
/** Default database name. One database holds one reactor's attachment store. */
|
|
801
|
+
export declare const DEFAULT_IDB_DATABASE = "ph-attachments";
|
|
802
|
+
type IdbAttachmentBackendOptions = {
|
|
803
|
+
/**
|
|
804
|
+
* Database name; defaults to {@link DEFAULT_IDB_DATABASE}. A host running
|
|
805
|
+
* several reactors in one origin must namespace this per reactor, exactly as
|
|
806
|
+
* the operation store is namespaced -- two reactors sharing one attachment
|
|
807
|
+
* database would share evictions and storage accounting.
|
|
808
|
+
*/
|
|
809
|
+
databaseName?: string;
|
|
810
|
+
/**
|
|
811
|
+
* The factory to open against; defaults to the realm's `indexedDB`. The seam
|
|
812
|
+
* a shim (or a test double) is injected through, and the reason this backend
|
|
813
|
+
* takes no dependency on a global being present at import time.
|
|
814
|
+
*/
|
|
815
|
+
indexedDB?: IDBFactory;
|
|
816
|
+
};
|
|
817
|
+
/**
|
|
818
|
+
* An IndexedDB-backed {@link ILocalAttachmentBackend}: the browser-resident
|
|
819
|
+
* attachment store a reactor in a tab or a SharedWorker replicates into.
|
|
820
|
+
*
|
|
821
|
+
* Two object stores, written in one transaction so a record and its blob land
|
|
822
|
+
* or fail together (the atomicity {@link ILocalAttachmentBackend.write}
|
|
823
|
+
* requires). Bytes are kept as `ArrayBuffer` rather than `Blob`: both are
|
|
824
|
+
* structured-clone-safe values IndexedDB stores natively, and an ArrayBuffer
|
|
825
|
+
* needs no async read to get back to the `Uint8Array` the store serves.
|
|
826
|
+
*
|
|
827
|
+
* OPFS is the obvious later alternative for large blobs -- it streams and does
|
|
828
|
+
* not hold a whole attachment in the realm's heap -- and nothing above this
|
|
829
|
+
* interface would change: it is one more backend.
|
|
830
|
+
*
|
|
831
|
+
* The database handle is opened lazily on first use and reused, so constructing
|
|
832
|
+
* this in a realm that has no `indexedDB` (a Node import of the barrel) costs
|
|
833
|
+
* nothing and fails only if something actually reads or writes.
|
|
834
|
+
*/
|
|
835
|
+
export declare class IdbAttachmentBackend implements ILocalAttachmentBackend {
|
|
836
|
+
private readonly databaseName;
|
|
837
|
+
private readonly factory;
|
|
838
|
+
private opening;
|
|
839
|
+
private database;
|
|
840
|
+
private closed;
|
|
841
|
+
constructor(options?: IdbAttachmentBackendOptions);
|
|
842
|
+
readRecord(hash: AttachmentHash): Promise<LocalAttachmentRecord | undefined>;
|
|
843
|
+
write(record: LocalAttachmentRecord, bytes: Uint8Array): Promise<void>;
|
|
844
|
+
readBytes(hash: AttachmentHash): Promise<Uint8Array | undefined>;
|
|
845
|
+
evict(record: LocalAttachmentRecord): Promise<void>;
|
|
846
|
+
touch(hash: AttachmentHash, lastAccessedAtUtc: string): Promise<void>;
|
|
847
|
+
availableBytes(): Promise<number>;
|
|
848
|
+
close(): Promise<void>;
|
|
849
|
+
private open;
|
|
850
|
+
private openDatabase;
|
|
851
|
+
}
|
|
852
|
+
//#endregion
|
|
853
|
+
//#region src/storage/local/local-attachment-store.d.ts
|
|
854
|
+
/**
|
|
855
|
+
* A realm-resident, content-addressed {@link IAttachmentStore} over a
|
|
856
|
+
* {@link ILocalAttachmentBackend} -- the browser-capable half of attachment
|
|
857
|
+
* byte movement.
|
|
858
|
+
*
|
|
859
|
+
* The same class serves both backends, so an IndexedDB store in a tab and the
|
|
860
|
+
* memory twin a Node test runs cannot diverge in behaviour: every semantic the
|
|
861
|
+
* `IAttachmentStore` contract states is implemented once, here.
|
|
862
|
+
*
|
|
863
|
+
* Differences from `KyselyAttachmentStore`, all of them deliberate:
|
|
864
|
+
*
|
|
865
|
+
* - No reservation table, so `stat()`/`get()` never synthesize `pending` from
|
|
866
|
+
* local state. `pending` only ever arrives as a TRANSPORT answer, which
|
|
867
|
+
* `get()` raises as {@link AttachmentPending} exactly as the Kysely store
|
|
868
|
+
* does for a remote pending.
|
|
869
|
+
* - `get()` reads the whole blob before it hands back a stream, so there is no
|
|
870
|
+
* such thing as an in-flight read for `evict()` to destroy. The contract's
|
|
871
|
+
* "skip hashes with active readers" clause is satisfied by construction
|
|
872
|
+
* rather than by a refcount; a caller already holding a stream holds a
|
|
873
|
+
* snapshot.
|
|
874
|
+
* - `sizeBytes` is the measured length of the bytes received, not the
|
|
875
|
+
* producer's `AttachmentMetadata.sizeBytes` claim, so `storageUsed()` cannot
|
|
876
|
+
* be talked into a wrong number by a remote.
|
|
877
|
+
*/
|
|
878
|
+
export declare class LocalAttachmentStore implements IAttachmentStore {
|
|
879
|
+
private readonly backend;
|
|
880
|
+
private readonly transport;
|
|
881
|
+
private readonly now;
|
|
882
|
+
constructor(backend: ILocalAttachmentBackend, transport: IAttachmentTransport, now?: () => Date);
|
|
883
|
+
stat(hash: AttachmentHash): Promise<AttachmentHeader>;
|
|
884
|
+
has(hash: AttachmentHash): Promise<boolean>;
|
|
885
|
+
get(hash: AttachmentHash, signal?: AbortSignal, documentId?: string): Promise<AttachmentResponse>;
|
|
886
|
+
put(hash: AttachmentHash, metadata: AttachmentMetadata, data: ReadableStream<Uint8Array>): Promise<void>;
|
|
887
|
+
evict(hash: AttachmentHash): Promise<void>;
|
|
888
|
+
storageUsed(): Promise<number>;
|
|
889
|
+
/** Releases the backend handle. */
|
|
890
|
+
close(): Promise<void>;
|
|
891
|
+
/**
|
|
892
|
+
* Stores bytes this realm produced itself, as `source: "local"`.
|
|
893
|
+
*
|
|
894
|
+
* `put()` is the transport's entry point and marks everything `sync`, which
|
|
895
|
+
* is the right provenance for a replicated blob and the wrong one for a blob
|
|
896
|
+
* that originated here. The server-side store gets `source: "local"` rows
|
|
897
|
+
* from its upload handles; a local store has no upload handle, so this is
|
|
898
|
+
* how a producer (an editor that just hashed a file, a test that seeds a
|
|
899
|
+
* peer) says the bytes are its own.
|
|
900
|
+
*/
|
|
901
|
+
putLocal(hash: AttachmentHash, metadata: AttachmentMetadata, data: ReadableStream<Uint8Array>): Promise<void>;
|
|
902
|
+
/**
|
|
903
|
+
* `overwrite` skips the available-hash dedup check.
|
|
904
|
+
*
|
|
905
|
+
* Only {@link fetchRemote} sets it, and only because it is reached when the
|
|
906
|
+
* local copy is unusable -- including the torn case where the record says
|
|
907
|
+
* `available` over a missing blob. Without it, that record would deduplicate
|
|
908
|
+
* away its own repair and the hash could never be restored.
|
|
909
|
+
*/
|
|
910
|
+
private store;
|
|
911
|
+
/**
|
|
912
|
+
* Restores `hash` from the transport on behalf of `documentId`, persists it
|
|
913
|
+
* through the same write path `put` uses (so provenance follows one rule)
|
|
914
|
+
* and serves the result from the store rather than from the transport's
|
|
915
|
+
* stream, which has already been consumed.
|
|
916
|
+
*
|
|
917
|
+
* The fetched bytes are hashed and refused when they are not what was asked
|
|
918
|
+
* for, mirroring {@link AttachmentReplicator}: a transport peer is another
|
|
919
|
+
* reactor rather than a trusted server, and a content-addressed store that
|
|
920
|
+
* accepts bytes it never verified is no longer content-addressed. A mismatch
|
|
921
|
+
* is surfaced as an error and nothing is written, so a lying peer cannot
|
|
922
|
+
* poison the store.
|
|
923
|
+
*/
|
|
924
|
+
private fetchRemote;
|
|
925
|
+
}
|
|
926
|
+
//#endregion
|
|
927
|
+
//#region src/storage/local/memory-attachment-backend.d.ts
|
|
928
|
+
/**
|
|
929
|
+
* The in-realm memory twin of {@link IdbAttachmentBackend}.
|
|
930
|
+
*
|
|
931
|
+
* It exists so the whole {@link LocalAttachmentStore} contract -- and the
|
|
932
|
+
* replicator above it -- is testable in Node without an IndexedDB shim, and so
|
|
933
|
+
* a reactor that wants a deliberately ephemeral attachment store (a test, a
|
|
934
|
+
* throwaway reactor) has one. The backend contract suite runs against
|
|
935
|
+
* this and against the IndexedDB backend, so the two answer identically.
|
|
936
|
+
*
|
|
937
|
+
* Records are copied in and out: a caller mutating a returned record must not
|
|
938
|
+
* be able to change what the store holds, which is the behaviour a real
|
|
939
|
+
* persistence layer has for free.
|
|
940
|
+
*/
|
|
941
|
+
export declare class MemoryAttachmentBackend implements ILocalAttachmentBackend {
|
|
942
|
+
private readonly records;
|
|
943
|
+
private readonly blobs;
|
|
944
|
+
readRecord(hash: AttachmentHash): Promise<LocalAttachmentRecord | undefined>;
|
|
945
|
+
write(record: LocalAttachmentRecord, bytes: Uint8Array): Promise<void>;
|
|
946
|
+
readBytes(hash: AttachmentHash): Promise<Uint8Array | undefined>;
|
|
947
|
+
evict(record: LocalAttachmentRecord): Promise<void>;
|
|
948
|
+
touch(hash: AttachmentHash, lastAccessedAtUtc: string): Promise<void>;
|
|
949
|
+
availableBytes(): Promise<number>;
|
|
950
|
+
close(): Promise<void>;
|
|
951
|
+
}
|
|
952
|
+
//#endregion
|
|
953
|
+
export type { AttachmentPeerLink, AttachmentPeerLinkOptions, AttachmentPeerLinksOptions, AttachmentReadGate, AttachmentReferencePageResult, AttachmentReferenceRow, AttachmentReplicationEntry, AttachmentReplicationState, AttachmentReplicatorOptions, AttachmentReplicatorStatus, AttachmentRetryPolicy, AttachmentServedStats, IAttachmentReferenceScanner, IDocumentScopeGate, ILocalAttachmentBackend, IOperationAttachmentRefs, IdbAttachmentBackendOptions, LocalAttachmentAuthorizer, LocalAttachmentBeginResponse, LocalAttachmentCancelRequest, LocalAttachmentChunkResponse, LocalAttachmentEndResponse, LocalAttachmentErrorResponse, LocalAttachmentFetchRequest, LocalAttachmentMessage, LocalAttachmentNotFoundResponse, LocalAttachmentPendingResponse, LocalAttachmentRecord, LocalAttachmentRequest, LocalAttachmentResponse, LocalAttachmentServerOptions, LocalAttachmentTransportOptions, PeeredAttachmentTransportOptions, ReadGateAttachmentAuthorizerOptions, ReplicationTimers };
|
|
954
|
+
export {};
|
|
955
|
+
//# sourceMappingURL=replication.d.ts.map
|