@enbox/agent 0.7.8 → 0.7.9

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.
@@ -1,7 +1,7 @@
1
1
  import type { AbstractLevel } from 'abstract-level';
2
2
 
3
3
  import type { DwnSubscriptionHandler, ResubscribeFactory } from '@enbox/dwn-clients';
4
- import type { GenericMessage, MessageEvent, MessagesSubscribeReply, MessagesSyncDependencyEntry, MessagesSyncDiffEntry, MessagesSyncReply, ProgressToken, ProtocolsConfigureMessage, RecordsProjectionScope, RecordsWriteMessage, StateIndex, SubscriptionMessage } from '@enbox/dwn-sdk-js';
4
+ import type { GenericMessage, MessageEvent, MessagesSubscribeReply, MessagesSyncDependencyEntry, MessagesSyncDiffEntry, MessagesSyncReply, ProgressToken, ProtocolsConfigureMessage, ProtocolsQueryReply, RecordsProjectionScope, RecordsWriteMessage, StateIndex, SubscriptionMessage } from '@enbox/dwn-sdk-js';
5
5
 
6
6
  import ms from 'ms';
7
7
 
@@ -9,15 +9,15 @@ import { Level } from 'level';
9
9
  import { sleep } from '@enbox/common';
10
10
  import { authenticate, DwnInterfaceName, DwnMethodName, Encoder, hashToHex, initDefaultHashes, Message, ProtocolsConfigure, RECORDS_PROJECTION_ROOT_VERSION, RecordsProjection, RecordsWrite } from '@enbox/dwn-sdk-js';
11
11
 
12
- import type { ClosureEvaluationContext } from './sync-closure-types.js';
12
+ import type { DwnMessageParams } from './types/dwn.js';
13
13
  import type { EnboxPlatformAgent } from './types/agent.js';
14
14
  import type { PermissionsApi } from './types/permissions.js';
15
15
  import type { SyncMessageEntry } from './sync-messages.js';
16
16
  import type { SyncScopeClassification } from './sync-scope-acceptance.js';
17
+ import type { ClosureEvaluationContext, ClosureResult } from './sync-closure-types.js';
17
18
  import type { DeadLetterCategory, DeadLetterEntry, NonEmptyStringArray, PushResult, ReplicationLinkState, StartSyncParams, SyncAuthorization, SyncConnectivityState, SyncEngine, SyncEvent, SyncEventListener, SyncEventScope, SyncHealthSummary, SyncIdentityOptions, SyncMode, SyncScope } from './types/sync.js';
18
19
 
19
20
  import { AgentPermissionsApi } from './permissions-api.js';
20
- import type { DwnMessageParams } from './types/dwn.js';
21
21
 
22
22
  import { buildLinkId } from './sync-link-id.js';
23
23
  import { DwnInterface } from './types/dwn.js';
@@ -26,8 +26,8 @@ import { isRecordsWrite } from './utils.js';
26
26
  import { ReplicationLedger } from './sync-replication-ledger.js';
27
27
  import { topologicalSort } from './sync-topological-sort.js';
28
28
  import { classifySyncEventScope, classifySyncMessageScope } from './sync-scope-acceptance.js';
29
+ import { ClosureFailureCode, createClosureContext, invalidateClosureCache, isTerminalClosureFailureCode } from './sync-closure-types.js';
29
30
  import { computeAuthorizationEpoch, computeProjectionId, lexicographicalCompare, MAX_PENDING_TOKENS, protocolsForSyncScope, singleProtocolForSyncScope, syncScopeFromProtocols } from './types/sync.js';
30
- import { createClosureContext, invalidateClosureCache, isTerminalClosureFailureCode } from './sync-closure-types.js';
31
31
  import { fetchRemoteMessages, getMessageCid, pullMessages, pushMessages, SyncPullAbortedError } from './sync-messages.js';
32
32
  import { partitionRemoteEntries, SyncLinkReconciler } from './sync-link-reconciler.js';
33
33
  import { permissionGrantIdsFromEntries, resolveMessagesSyncScopes, toMessagesPermissionGrantIds, toSyncAuthorizationGrants } from './sync-permission-grants.js';
@@ -242,6 +242,13 @@ type PullDelivery = {
242
242
  ordinal: number;
243
243
  };
244
244
 
245
+ type LivePullDataStreamFactory = () => Promise<ReadableStream<Uint8Array> | undefined>;
246
+
247
+ type ApplyStatus = {
248
+ code: number;
249
+ detail?: string;
250
+ };
251
+
245
252
  function syncEventScope(scope: SyncScope | undefined): SyncEventScope {
246
253
  if (scope === undefined) {
247
254
  return {};
@@ -353,6 +360,9 @@ export class SyncEngineLevel implements SyncEngine {
353
360
  */
354
361
  private readonly _closureContexts: Map<string, ClosureEvaluationContext> = new Map();
355
362
 
363
+ /** Deduplicates concurrent live-sync repairs for the same tenant/protocol. */
364
+ private readonly _protocolMetadataRepairs: Map<string, Promise<boolean>> = new Map();
365
+
356
366
  /** Maximum entries in the echo-loop suppression cache. */
357
367
  private static readonly ECHO_SUPPRESS_MAX_ENTRIES = 10_000;
358
368
 
@@ -1533,6 +1543,7 @@ export class SyncEngineLevel implements SyncEngine {
1533
1543
 
1534
1544
  // Clear closure evaluation contexts.
1535
1545
  this._closureContexts.clear();
1546
+ this._protocolMetadataRepairs.clear();
1536
1547
  this._recentlyPulledCids.clear();
1537
1548
 
1538
1549
  // Clear the in-memory link and runtime state.
@@ -2163,42 +2174,132 @@ export class SyncEngineLevel implements SyncEngine {
2163
2174
  }
2164
2175
 
2165
2176
  private async processLivePullEvent(context: LivePullContext, event: MessageEvent): Promise<string | undefined> {
2166
- const dataStream = await this.getLivePullDataStream(context, event);
2167
- await this.agent.dwn.processRawMessage(context.did, event.message, { dataStream });
2177
+ const dataStreamFactory = await this.createLivePullDataStreamFactory(context, event);
2178
+ let applyStatus = await this.applyLivePullEvent(context, event, dataStreamFactory);
2168
2179
  if (context.isStale()) { return undefined; }
2169
2180
 
2170
- this.invalidateClosureCacheForMessage(context.did, event.message);
2181
+ let applied = SyncEngineLevel.isApplySuccess(applyStatus.code);
2182
+ if (applied) {
2183
+ this.invalidateClosureCacheForMessage(context.did, event.message);
2184
+ }
2185
+
2171
2186
  if (!await this.ensureClosureComplete(context, event)) {
2172
2187
  return undefined;
2173
2188
  }
2174
2189
 
2190
+ if (!applied) {
2191
+ applyStatus = await this.applyLivePullEvent(context, event, dataStreamFactory);
2192
+ if (context.isStale()) { return undefined; }
2193
+
2194
+ applied = SyncEngineLevel.isApplySuccess(applyStatus.code);
2195
+ if (!applied) {
2196
+ throw await this.createLivePullApplyError(event, applyStatus);
2197
+ }
2198
+ this.invalidateClosureCacheForMessage(context.did, event.message);
2199
+ }
2200
+
2175
2201
  // Squash convergence: processRawMessage triggers the DWN's built-in
2176
2202
  // squash resumable task (performRecordsSquash), so no additional
2177
2203
  // sync-engine side effect is needed here.
2178
2204
  return Message.getCid(event.message);
2179
2205
  }
2180
2206
 
2181
- private async getLivePullDataStream(
2182
- { did, dwnUrl, delegateDid, permissionGrantIds }: LivePullContext,
2207
+ private async applyLivePullEvent(
2208
+ context: LivePullContext,
2183
2209
  event: MessageEvent,
2184
- ): Promise<ReadableStream<Uint8Array> | undefined> {
2185
- const inlineData = this.extractDataStream(event);
2186
- if (inlineData || !isRecordsWrite(event) || !(event.message.descriptor as any).dataCid) {
2187
- return inlineData;
2210
+ dataStreamFactory: LivePullDataStreamFactory,
2211
+ ): Promise<ApplyStatus> {
2212
+ const dataStream = await dataStreamFactory();
2213
+ const reply = await this.agent.dwn.processRawMessage(context.did, event.message, { dataStream });
2214
+ return reply.status;
2215
+ }
2216
+
2217
+ private async createLivePullDataStreamFactory(
2218
+ context: LivePullContext,
2219
+ event: MessageEvent,
2220
+ ): Promise<LivePullDataStreamFactory> {
2221
+ if (!isRecordsWrite(event)) {
2222
+ return async () => undefined;
2223
+ }
2224
+
2225
+ const encodedData = (event.message as any).encodedData as string | undefined;
2226
+ if (encodedData) {
2227
+ delete (event.message as any).encodedData;
2228
+ const bytes = Encoder.base64UrlToBytes(encodedData);
2229
+ return async () => SyncEngineLevel.dataStreamFromBytes(bytes);
2230
+ }
2231
+
2232
+ const eventData = (event as any).data as ReadableStream<Uint8Array> | undefined;
2233
+ if (eventData) {
2234
+ const bytes = await SyncEngineLevel.readStreamBytes(eventData);
2235
+ return async () => SyncEngineLevel.dataStreamFromBytes(bytes);
2236
+ }
2237
+
2238
+ if (!(event.message.descriptor as any).dataCid) {
2239
+ return async () => undefined;
2188
2240
  }
2189
2241
 
2190
2242
  // For large RecordsWrite messages (no inline data), fetch the data from
2191
- // the remote DWN via MessagesRead before storing locally.
2243
+ // the remote DWN via MessagesRead before each store attempt. ReadableStream
2244
+ // instances are single-use, so a repair-triggered retry needs a fresh fetch.
2245
+ const { did, dwnUrl, delegateDid, permissionGrantIds } = context;
2192
2246
  const messageCid = await Message.getCid(event.message);
2193
- const fetched = await fetchRemoteMessages({
2194
- did,
2195
- dwnUrl,
2196
- delegateDid,
2197
- permissionGrantIds,
2198
- messageCids : [messageCid],
2199
- agent : this.agent,
2247
+ return async () => {
2248
+ const fetched = await fetchRemoteMessages({
2249
+ did,
2250
+ dwnUrl,
2251
+ delegateDid,
2252
+ permissionGrantIds,
2253
+ messageCids : [messageCid],
2254
+ agent : this.agent,
2255
+ });
2256
+ return fetched[0]?.dataStream;
2257
+ };
2258
+ }
2259
+
2260
+ private async createLivePullApplyError(event: MessageEvent, status: ApplyStatus): Promise<Error> {
2261
+ const cid = await Message.getCid(event.message);
2262
+ return new Error(
2263
+ `SyncEngineLevel: live pull apply failed for ${cid}: ${status.code} ${status.detail ?? ''}`.trim()
2264
+ );
2265
+ }
2266
+
2267
+ private static dataStreamFromBytes(bytes: Uint8Array): ReadableStream<Uint8Array> {
2268
+ return new ReadableStream<Uint8Array>({
2269
+ start(controller): void {
2270
+ controller.enqueue(bytes);
2271
+ controller.close();
2272
+ }
2200
2273
  });
2201
- return fetched[0]?.dataStream;
2274
+ }
2275
+
2276
+ private static async readStreamBytes(stream: ReadableStream<Uint8Array>): Promise<Uint8Array> {
2277
+ const reader = stream.getReader();
2278
+ const chunks: Uint8Array[] = [];
2279
+ let totalSize = 0;
2280
+
2281
+ try {
2282
+ for (;;) {
2283
+ const { done, value } = await reader.read();
2284
+ if (done) { break; }
2285
+ chunks.push(value);
2286
+ totalSize += value.byteLength;
2287
+ }
2288
+ } finally {
2289
+ reader.releaseLock();
2290
+ }
2291
+
2292
+ const bytes = new Uint8Array(totalSize);
2293
+ let offset = 0;
2294
+ for (const chunk of chunks) {
2295
+ bytes.set(chunk, offset);
2296
+ offset += chunk.byteLength;
2297
+ }
2298
+ return bytes;
2299
+ }
2300
+
2301
+ private static isApplySuccess(code: number): boolean {
2302
+ return (code >= 200 && code < 300) || code === 409;
2202
2303
  }
2203
2304
 
2204
2305
  private invalidateClosureCacheForMessage(did: string, message: GenericMessage): void {
@@ -2225,14 +2326,279 @@ export class SyncEngineLevel implements SyncEngine {
2225
2326
  }
2226
2327
 
2227
2328
  const messageStore = this.agent.dwn.node.storage.messageStore;
2228
- const closureResult = await evaluateClosure(event.message, messageStore, link.scope, closureCtx);
2329
+ let closureResult = await evaluateClosure(event.message, messageStore, link.scope, closureCtx);
2229
2330
  if (isStale()) { return false; }
2230
2331
  if (closureResult.complete) { return true; }
2231
2332
 
2333
+ if (await this.tryRepairMissingProtocolMetadata(context, closureCtx, closureResult)) {
2334
+ if (isStale()) { return false; }
2335
+
2336
+ closureResult = await evaluateClosure(event.message, messageStore, link.scope, closureCtx);
2337
+ if (isStale()) { return false; }
2338
+ if (closureResult.complete) { return true; }
2339
+ }
2340
+
2232
2341
  await this.recordClosureFailure(context, event, closureResult.failure!.code, closureResult.failure!.detail);
2233
2342
  return false;
2234
2343
  }
2235
2344
 
2345
+ private async tryRepairMissingProtocolMetadata(
2346
+ context: LivePullContext,
2347
+ closureCtx: ClosureEvaluationContext,
2348
+ closureResult: ClosureResult,
2349
+ ): Promise<boolean> {
2350
+ const failure = closureResult.failure;
2351
+ if (!SyncEngineLevel.isRepairableProtocolMetadataFailure(failure)) {
2352
+ return false;
2353
+ }
2354
+
2355
+ const { did } = context;
2356
+ const repairKey = `${did}|${failure.edge.identifier}`;
2357
+ const activeRepair = this._protocolMetadataRepairs.get(repairKey);
2358
+ if (activeRepair) {
2359
+ return activeRepair;
2360
+ }
2361
+
2362
+ const repair = this.repairMissingProtocolMetadata(context, closureCtx, failure.edge.identifier);
2363
+ this._protocolMetadataRepairs.set(repairKey, repair);
2364
+ repair.finally(() => {
2365
+ if (this._protocolMetadataRepairs.get(repairKey) === repair) {
2366
+ this._protocolMetadataRepairs.delete(repairKey);
2367
+ }
2368
+ }).catch(() => { /* caller handles the repair result */ });
2369
+ return repair;
2370
+ }
2371
+
2372
+ private async repairMissingProtocolMetadata(
2373
+ { did, dwnUrl, delegateDid, isStale }: LivePullContext,
2374
+ closureCtx: ClosureEvaluationContext,
2375
+ protocol: string,
2376
+ ): Promise<boolean> {
2377
+ if (isStale()) {
2378
+ return false;
2379
+ }
2380
+
2381
+ const configs = await this.fetchRemoteProtocolConfigClosure({
2382
+ authorDid : delegateDid ?? did,
2383
+ delegateDid,
2384
+ dwnUrl,
2385
+ protocol,
2386
+ tenantDid : did,
2387
+ });
2388
+ if (isStale() || configs.length === 0) {
2389
+ return false;
2390
+ }
2391
+
2392
+ // Live subscriptions can deliver scoped records before the local replica
2393
+ // has the tenant's protocol metadata. Reuse the DWN ProtocolsQuery path and
2394
+ // only install configs that are signed by the tenant, including composed
2395
+ // protocol dependencies needed to authorize the record.
2396
+ let repaired = false;
2397
+ for (const config of configs) {
2398
+ if (isStale()) {
2399
+ return repaired;
2400
+ }
2401
+ const reply = await this.agent.dwn.processRawMessage(did, config);
2402
+ if (isStale()) {
2403
+ return repaired;
2404
+ }
2405
+ if (!SyncEngineLevel.protocolConfigApplySucceeded(reply.status.code)) {
2406
+ return repaired;
2407
+ }
2408
+
2409
+ invalidateClosureCache(closureCtx, config);
2410
+ repaired = true;
2411
+ }
2412
+
2413
+ return repaired;
2414
+ }
2415
+
2416
+ private async fetchRemoteProtocolConfigClosure({
2417
+ authorDid,
2418
+ delegateDid,
2419
+ dwnUrl,
2420
+ protocol,
2421
+ tenantDid,
2422
+ }: {
2423
+ authorDid: string;
2424
+ delegateDid?: string;
2425
+ dwnUrl: string;
2426
+ protocol: string;
2427
+ tenantDid: string;
2428
+ }): Promise<ProtocolsConfigureMessage[]> {
2429
+ const configsByProtocol = new Map<string, ProtocolsConfigureMessage>();
2430
+ const visiting = new Set<string>();
2431
+
2432
+ const visit = async (protocolUri: string): Promise<boolean> => {
2433
+ if (configsByProtocol.has(protocolUri)) {
2434
+ return true;
2435
+ }
2436
+ if (visiting.has(protocolUri)) {
2437
+ return true;
2438
+ }
2439
+ visiting.add(protocolUri);
2440
+
2441
+ const config = await this.fetchRemoteProtocolConfig({
2442
+ authorDid,
2443
+ delegateDid,
2444
+ dwnUrl,
2445
+ protocol: protocolUri,
2446
+ tenantDid,
2447
+ });
2448
+ if (config === undefined) {
2449
+ visiting.delete(protocolUri);
2450
+ return false;
2451
+ }
2452
+
2453
+ for (const usedProtocol of SyncEngineLevel.protocolsConfigureUses(config)) {
2454
+ if (!await visit(usedProtocol)) {
2455
+ visiting.delete(protocolUri);
2456
+ return false;
2457
+ }
2458
+ }
2459
+
2460
+ configsByProtocol.set(protocolUri, config);
2461
+ visiting.delete(protocolUri);
2462
+ return true;
2463
+ };
2464
+
2465
+ return await visit(protocol) ? [...configsByProtocol.values()] : [];
2466
+ }
2467
+
2468
+ private async fetchRemoteProtocolConfig({
2469
+ authorDid,
2470
+ delegateDid,
2471
+ dwnUrl,
2472
+ protocol,
2473
+ tenantDid,
2474
+ }: {
2475
+ authorDid: string;
2476
+ delegateDid?: string;
2477
+ dwnUrl: string;
2478
+ protocol: string;
2479
+ tenantDid: string;
2480
+ }): Promise<ProtocolsConfigureMessage | undefined> {
2481
+ try {
2482
+ const permissionGrantId = await this.getProtocolsQueryPermissionGrantId({
2483
+ delegateDid,
2484
+ protocol,
2485
+ tenantDid,
2486
+ });
2487
+ const { message } = await this.agent.processDwnRequest({
2488
+ author : authorDid,
2489
+ messageParams : {
2490
+ filter: { protocol },
2491
+ ...(permissionGrantId === undefined ? {} : { permissionGrantId }),
2492
+ },
2493
+ messageType : DwnInterface.ProtocolsQuery,
2494
+ store : false,
2495
+ target : tenantDid,
2496
+ });
2497
+
2498
+ const reply = await this.agent.rpc.sendDwnRequest({
2499
+ dwnUrl,
2500
+ message,
2501
+ targetDid: tenantDid,
2502
+ }) as ProtocolsQueryReply;
2503
+ if (reply.status.code !== 200 || reply.entries === undefined) {
2504
+ return undefined;
2505
+ }
2506
+
2507
+ const candidates: ProtocolsConfigureMessage[] = [];
2508
+ for (const entry of reply.entries) {
2509
+ const config = await this.toAuthenticatedTenantProtocolConfig(tenantDid, entry);
2510
+ if (config?.descriptor.definition.protocol === protocol) {
2511
+ candidates.push(config);
2512
+ }
2513
+ }
2514
+
2515
+ return SyncEngineLevel.newestProtocolConfig(candidates);
2516
+ } catch {
2517
+ return undefined;
2518
+ }
2519
+ }
2520
+
2521
+ private async getProtocolsQueryPermissionGrantId({
2522
+ delegateDid,
2523
+ protocol,
2524
+ tenantDid,
2525
+ }: {
2526
+ delegateDid?: string;
2527
+ protocol: string;
2528
+ tenantDid: string;
2529
+ }): Promise<string | undefined> {
2530
+ if (delegateDid === undefined) {
2531
+ return undefined;
2532
+ }
2533
+
2534
+ try {
2535
+ const { grant } = await this._permissionsApi.getPermissionForRequest({
2536
+ connectedDid : tenantDid,
2537
+ delegateDid,
2538
+ protocol,
2539
+ cached : true,
2540
+ messageType : DwnInterface.ProtocolsQuery,
2541
+ });
2542
+ return grant.id;
2543
+ } catch {
2544
+ return undefined;
2545
+ }
2546
+ }
2547
+
2548
+ private async toAuthenticatedTenantProtocolConfig(
2549
+ tenantDid: string,
2550
+ message: GenericMessage,
2551
+ ): Promise<ProtocolsConfigureMessage | undefined> {
2552
+ if (!SyncEngineLevel.isProtocolsConfigureDefinitionMessage(message)) {
2553
+ return undefined;
2554
+ }
2555
+
2556
+ try {
2557
+ await ProtocolsConfigure.parse(message);
2558
+ if (Message.getAuthor(message) !== tenantDid) {
2559
+ return undefined;
2560
+ }
2561
+ await authenticate(message.authorization, this.agent.did);
2562
+ return message;
2563
+ } catch {
2564
+ return undefined;
2565
+ }
2566
+ }
2567
+
2568
+ private static newestProtocolConfig(
2569
+ configs: ProtocolsConfigureMessage[],
2570
+ ): ProtocolsConfigureMessage | undefined {
2571
+ let newest: ProtocolsConfigureMessage | undefined;
2572
+ for (const config of configs) {
2573
+ if (newest === undefined || SyncEngineLevel.isProtocolConfigNewer(config, newest)) {
2574
+ newest = config;
2575
+ }
2576
+ }
2577
+ return newest;
2578
+ }
2579
+
2580
+ private static isProtocolConfigNewer(
2581
+ candidate: ProtocolsConfigureMessage,
2582
+ current: ProtocolsConfigureMessage,
2583
+ ): boolean {
2584
+ return candidate.descriptor.messageTimestamp > current.descriptor.messageTimestamp;
2585
+ }
2586
+
2587
+ private static protocolConfigApplySucceeded(code: number): boolean {
2588
+ return (code >= 200 && code < 300) || code === 409;
2589
+ }
2590
+
2591
+ private static isRepairableProtocolMetadataFailure(
2592
+ failure: ClosureResult['failure'] | undefined,
2593
+ ): failure is NonNullable<ClosureResult['failure']> {
2594
+ return failure?.edge.identifierType === 'protocol' &&
2595
+ (
2596
+ failure.code === ClosureFailureCode.ProtocolMetadataMissing ||
2597
+ failure.code === ClosureFailureCode.CrossProtocolReferenceMissing ||
2598
+ failure.code === ClosureFailureCode.EncryptionDependencyMissing
2599
+ );
2600
+ }
2601
+
2236
2602
  private async recordClosureFailure(
2237
2603
  { did, dwnUrl, linkKey, link, isStale }: LivePullContext,
2238
2604
  event: MessageEvent,
@@ -3263,20 +3629,12 @@ export class SyncEngineLevel implements SyncEngine {
3263
3629
  tenantDid: string,
3264
3630
  dependency: SyncDependencyEntryWithMessage,
3265
3631
  ): Promise<AuthenticatedProtocolConfigDependency | undefined> {
3266
- if (!SyncEngineLevel.isProtocolsConfigureDefinitionMessage(dependency.message)) {
3632
+ const config = await this.toAuthenticatedTenantProtocolConfig(tenantDid, dependency.message);
3633
+ if (config === undefined) {
3267
3634
  return undefined;
3268
3635
  }
3269
3636
 
3270
- try {
3271
- await ProtocolsConfigure.parse(dependency.message);
3272
- if (Message.getAuthor(dependency.message) !== tenantDid) {
3273
- return undefined;
3274
- }
3275
- await authenticate(dependency.message.authorization, this.agent.did);
3276
- return { ...dependency, message: dependency.message };
3277
- } catch {
3278
- return undefined;
3279
- }
3637
+ return { ...dependency, message: config };
3280
3638
  }
3281
3639
 
3282
3640
  private static async projectedDependencyCidsMatch({
@@ -3775,43 +4133,6 @@ export class SyncEngineLevel implements SyncEngine {
3775
4133
  return buildLinkId(did, dwnUrl, projectionId, authorizationEpoch);
3776
4134
  }
3777
4135
 
3778
- // ---------------------------------------------------------------------------
3779
- // Utility helpers
3780
- // ---------------------------------------------------------------------------
3781
-
3782
- /**
3783
- * Extracts a ReadableStream from a MessageEvent if it contains a
3784
- * RecordsWrite with data — either as an inline `encodedData` field
3785
- * (for records <= 30 KB) or as a pre-existing data stream.
3786
- */
3787
- private extractDataStream(event: MessageEvent): ReadableStream<Uint8Array> | undefined {
3788
- if (!isRecordsWrite(event)) {
3789
- return undefined;
3790
- }
3791
-
3792
- // Check for inline base64url-encoded data (small records from EventLog).
3793
- // Delete the transport-level field so the DWN schema validator does not
3794
- // reject the message for having unevaluated properties.
3795
- const encodedData = (event.message as any).encodedData as string | undefined;
3796
- if (encodedData) {
3797
- delete (event.message as any).encodedData;
3798
- const bytes = Encoder.base64UrlToBytes(encodedData);
3799
- return new ReadableStream<Uint8Array>({
3800
- start(controller): void {
3801
- controller.enqueue(bytes);
3802
- controller.close();
3803
- }
3804
- });
3805
- }
3806
-
3807
- // Check for a pre-existing data stream (e.g. from a direct message read).
3808
- if ((event as any).data) {
3809
- return (event as any).data;
3810
- }
3811
-
3812
- return undefined;
3813
- }
3814
-
3815
4136
  // ---------------------------------------------------------------------------
3816
4137
  // Default Hash Cache
3817
4138
  // ---------------------------------------------------------------------------