@relayfile/sdk 0.7.10 → 0.7.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -81,7 +81,11 @@ import { RelayFileClient } from "@relayfile/sdk";
81
81
 
82
82
  const client = new RelayFileClient({
83
83
  baseUrl: "https://api.relayfile.com",
84
- token: process.env.RELAYFILE_TOKEN ?? ""
84
+ token: process.env.RELAYFILE_TOKEN ?? "",
85
+ changeLog: {
86
+ retentionMs: 7 * 24 * 60 * 60 * 1000,
87
+ maxEntries: 10_000
88
+ }
85
89
  });
86
90
 
87
91
  const workspaceId = "workspace_123";
@@ -108,6 +112,8 @@ await client.writeFile({
108
112
 
109
113
  Use a relayfile JWT whose claims include `workspace_id`, `agent_name`, and `aud: ["relayfile"]`. The SDK adds `X-Correlation-Id` automatically for API calls.
110
114
 
115
+ The optional `changeLog` block configures the SDK's local per-workspace retained-change mirror used by `subscribe()`, `open({ replayOnStart })`, and `getResourceAtEvent(eventId)`. Durable retention still lives on the Relayfile backend.
116
+
111
117
  ## Full Docs
112
118
 
113
119
  Full documentation is available in the [relayfile docs](https://github.com/AgentWorkforce/relayfile/tree/main/docs).
package/dist/client.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type AdminIngressStatusResponse, type AdminSyncStatusResponse, type BulkWriteInput, type BulkWriteResponse, type BackendStatusResponse, type AckResponse, type CommitForkInput, type CommitForkResponse, type CreateForkInput, type DeleteFileInput, type DeadLetterItem, type DeadLetterFeedResponse, type DiscardForkInput, type EventFeedResponse, type ExportJsonResponse, type ExportOptions, type FileQueryResponse, type FileReadResponse, type FilesystemEvent, type GetEventsOptions, type GetAdminIngressStatusOptions, type GetAdminSyncStatusOptions, type GetOperationsOptions, type GetSyncDeadLettersOptions, type GetSyncIngressStatusOptions, type GetSyncStatusOptions, type ListTreeOptions, type OperationFeedResponse, type OperationStatusResponse, type QueuedResponse, type ReadFileInput, type QueryFilesOptions, type SyncIngressStatusResponse, type SyncStatusResponse, type TreeResponse, type WriteFileInput, type WriteQueuedResponse, type IngestWebhookInput, type WritebackItem, type AckWritebackInput, type AckWritebackResponse } from "./types.js";
1
+ import { type AdminIngressStatusResponse, type AdminSyncStatusResponse, type BulkWriteInput, type BulkWriteResponse, type BackendStatusResponse, type AckResponse, type CommitForkInput, type CommitForkResponse, type CreateForkInput, type DeleteFileInput, type DeadLetterItem, type DeadLetterFeedResponse, type DiscardForkInput, type EventFeedResponse, type ExportJsonResponse, type ExportOptions, type FileQueryResponse, type FileReadResponse, type FilesystemEvent, type GetEventsOptions, type GetAdminIngressStatusOptions, type GetAdminSyncStatusOptions, type GetOperationsOptions, type GetSyncDeadLettersOptions, type GetSyncIngressStatusOptions, type GetSyncStatusOptions, type ListTreeOptions, type OperationFeedResponse, type OperationStatusResponse, type QueuedResponse, type ResourceAtEventResult, type ReadFileInput, type QueryFilesOptions, type Subscription, type SyncIngressStatusResponse, type SyncStatusResponse, type TreeResponse, type WriteFileInput, type WriteQueuedResponse, type IngestWebhookInput, type WritebackItem, type AckWritebackInput, type AckWritebackResponse, type ChangeEvent, type ChangeLogQueryResult, type ChangeStreamConnection, type ChangeStreamConnectionOptions, type SubscribeOptions } from "./types.js";
2
2
  import type { ForkHandle } from "@relayfile/core";
3
3
  /**
4
4
  * Bearer token or token factory used for Relayfile API requests.
@@ -13,6 +13,18 @@ export interface RelayFileRetryOptions {
13
13
  maxDelayMs?: number;
14
14
  jitterRatio?: number;
15
15
  }
16
+ export interface RelayFileChangeLogOptions {
17
+ /**
18
+ * Local retained-change cache TTL in milliseconds.
19
+ *
20
+ * This mirrors the backend retention window opportunistically for delivered
21
+ * or replayed events; durable change-log retention remains a server-side
22
+ * responsibility.
23
+ */
24
+ retentionMs?: number;
25
+ /** Maximum number of retained change entries to keep per workspace locally. */
26
+ maxEntries?: number;
27
+ }
16
28
  /** Default base URL for the hosted Relayfile API */
17
29
  export declare const DEFAULT_RELAYFILE_BASE_URL = "https://api.relayfile.dev";
18
30
  export interface RelayFileClientOptions {
@@ -28,6 +40,7 @@ export interface RelayFileClientOptions {
28
40
  fetchImpl?: typeof fetch;
29
41
  userAgent?: string;
30
42
  retry?: RelayFileRetryOptions;
43
+ changeLog?: RelayFileChangeLogOptions;
31
44
  }
32
45
  type WebSocketEventName = "event" | "error" | "open" | "close";
33
46
  type WebSocketHandlerMap = {
@@ -44,6 +57,10 @@ export interface ConnectWebSocketOptions {
44
57
  token?: string;
45
58
  onEvent?: (event: FilesystemEvent) => void;
46
59
  }
60
+ interface ProactiveRequestContext {
61
+ workspaceId: string;
62
+ token?: string;
63
+ }
47
64
  export declare class RelayFileClient {
48
65
  private readonly baseUrl;
49
66
  private readonly tokenProvider;
@@ -82,6 +99,11 @@ export declare class RelayFileClient {
82
99
  discardFork(input: DiscardForkInput): Promise<void>;
83
100
  commitFork(input: CommitForkInput): Promise<CommitForkResponse>;
84
101
  getEvents(workspaceId: string, options?: GetEventsOptions): Promise<EventFeedResponse>;
102
+ subscribe(globs: string[], onChange: (event: ChangeEvent) => void, options?: SubscribeOptions): Subscription;
103
+ open(options: ChangeStreamConnectionOptions): ChangeStreamConnection;
104
+ getResourceAtEvent(eventId: string, context?: ProactiveRequestContext): Promise<ResourceAtEventResult>;
105
+ listChangesSince(isoTimestamp: string, context?: ProactiveRequestContext): Promise<ChangeLogQueryResult>;
106
+ listLastNChanges(limit: number, context?: ProactiveRequestContext): Promise<ChangeLogQueryResult>;
85
107
  exportWorkspace(options: ExportOptions): Promise<ExportJsonResponse | Blob>;
86
108
  connectWebSocket(workspaceId: string, options?: ConnectWebSocketOptions): WebSocketConnection;
87
109
  getOp(workspaceId: string, opId: string, correlationId?: string, signal?: AbortSignal): Promise<OperationStatusResponse>;
@@ -102,6 +124,9 @@ export declare class RelayFileClient {
102
124
  ingestWebhook(input: IngestWebhookInput): Promise<QueuedResponse>;
103
125
  listPendingWritebacks(workspaceId: string, correlationId?: string, signal?: AbortSignal): Promise<WritebackItem[]>;
104
126
  ackWriteback(input: AckWritebackInput): Promise<AckWritebackResponse>;
127
+ private cacheWireChangeEvent;
128
+ private primeReplayCache;
129
+ private resolveWorkspaceId;
105
130
  private request;
106
131
  private performRequest;
107
132
  private shouldRetryStatus;
package/dist/client.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { RelayFileSync } from "./sync.js";
1
2
  import { InvalidStateError, PayloadTooLargeError, QueueFullError, RelayFileApiError, RevisionConflictError } from "./errors.js";
2
3
  /** Default base URL for the hosted Relayfile API */
3
4
  export const DEFAULT_RELAYFILE_BASE_URL = "https://api.relayfile.dev";
@@ -7,6 +8,20 @@ const DEFAULT_RETRY_OPTIONS = {
7
8
  maxDelayMs: 2000,
8
9
  jitterRatio: 0.2
9
10
  };
11
+ const DEFAULT_CHANGE_COALESCE_MS = 200;
12
+ const DEFAULT_CHANGE_LOG_RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
13
+ const DEFAULT_CHANGE_LOG_MAX_ENTRIES = 10_000;
14
+ const CLIENT_TOKEN_STREAM_KEY = "__client__";
15
+ const changeStreamManagers = new WeakMap();
16
+ const changeLogCaches = new WeakMap();
17
+ const changeLogSettings = new WeakMap();
18
+ const pendingChangeHydrations = new WeakMap();
19
+ function createM2NotImplementedError(feature) {
20
+ const error = new Error(`M2_NOT_IMPLEMENTED: ${feature} is reserved for proactive runtime M2.`);
21
+ error.name = "M2NotImplementedError";
22
+ error.code = "M2_NOT_IMPLEMENTED";
23
+ return error;
24
+ }
10
25
  function normalizeRetryOptions(options) {
11
26
  const maxRetries = options?.maxRetries ?? DEFAULT_RETRY_OPTIONS.maxRetries;
12
27
  const baseDelayMs = options?.baseDelayMs ?? DEFAULT_RETRY_OPTIONS.baseDelayMs;
@@ -19,6 +34,18 @@ function normalizeRetryOptions(options) {
19
34
  jitterRatio: Math.max(0, Math.min(1, jitterRatio))
20
35
  };
21
36
  }
37
+ function normalizeChangeLogOptions(options) {
38
+ const retentionMs = options?.retentionMs ?? DEFAULT_CHANGE_LOG_RETENTION_MS;
39
+ const maxEntries = options?.maxEntries ?? DEFAULT_CHANGE_LOG_MAX_ENTRIES;
40
+ return {
41
+ retentionMs: Number.isFinite(retentionMs) && retentionMs >= 0
42
+ ? Math.floor(retentionMs)
43
+ : DEFAULT_CHANGE_LOG_RETENTION_MS,
44
+ maxEntries: Number.isFinite(maxEntries) && maxEntries > 0
45
+ ? Math.floor(maxEntries)
46
+ : DEFAULT_CHANGE_LOG_MAX_ENTRIES
47
+ };
48
+ }
22
49
  function buildQuery(params) {
23
50
  const query = new URLSearchParams();
24
51
  for (const [key, value] of Object.entries(params)) {
@@ -158,6 +185,749 @@ class RelayFileWebSocketConnection {
158
185
  };
159
186
  }
160
187
  }
188
+ class WorkspaceChangeLogCache {
189
+ retentionMs;
190
+ maxEntries;
191
+ entries = [];
192
+ byId = new Map();
193
+ constructor(retentionMs = DEFAULT_CHANGE_LOG_RETENTION_MS, maxEntries = DEFAULT_CHANGE_LOG_MAX_ENTRIES) {
194
+ this.retentionMs = retentionMs;
195
+ this.maxEntries = maxEntries;
196
+ }
197
+ record(record) {
198
+ this.prune(Date.now());
199
+ const existing = this.byId.get(record.wire.id);
200
+ const merged = existing
201
+ ? {
202
+ wire: record.wire,
203
+ resource: record.resource.data !== null ? record.resource : existing.resource,
204
+ storedAt: record.storedAt,
205
+ context: record.context ?? existing.context
206
+ }
207
+ : record;
208
+ if (existing) {
209
+ const index = this.entries.findIndex((entry) => entry.wire.id === merged.wire.id);
210
+ if (index >= 0) {
211
+ this.entries.splice(index, 1);
212
+ }
213
+ this.entries.push(merged);
214
+ this.byId.set(merged.wire.id, merged);
215
+ return merged;
216
+ }
217
+ this.entries.push(merged);
218
+ this.byId.set(merged.wire.id, merged);
219
+ while (this.entries.length > this.maxEntries) {
220
+ const removed = this.entries.shift();
221
+ if (removed) {
222
+ this.byId.delete(removed.wire.id);
223
+ }
224
+ }
225
+ return merged;
226
+ }
227
+ get(eventId) {
228
+ this.prune(Date.now());
229
+ return this.byId.get(eventId);
230
+ }
231
+ listSince(isoTimestamp) {
232
+ this.prune(Date.now());
233
+ const threshold = Date.parse(isoTimestamp);
234
+ if (!Number.isFinite(threshold)) {
235
+ throw new Error(`Invalid ISO timestamp: ${isoTimestamp}`);
236
+ }
237
+ return this.entries.filter((entry) => Date.parse(entry.wire.occurredAt) >= threshold);
238
+ }
239
+ listLastN(limit) {
240
+ this.prune(Date.now());
241
+ const safeLimit = Math.max(0, Math.floor(limit));
242
+ if (safeLimit === 0) {
243
+ return [];
244
+ }
245
+ return this.entries.slice(-safeLimit);
246
+ }
247
+ prune(now) {
248
+ while (this.entries.length > 0) {
249
+ const first = this.entries[0];
250
+ if (!first) {
251
+ return;
252
+ }
253
+ if (now - first.storedAt < this.retentionMs) {
254
+ return;
255
+ }
256
+ this.entries.shift();
257
+ this.byId.delete(first.wire.id);
258
+ }
259
+ }
260
+ }
261
+ class RelayFileChangeSubscription {
262
+ manager;
263
+ onChange;
264
+ options;
265
+ active = true;
266
+ globPatterns;
267
+ pathScopes;
268
+ inFlight = new Set();
269
+ pendingByPath = new Map();
270
+ coalesceMs;
271
+ shouldCoalesce;
272
+ constructor(manager, globs, onChange, options) {
273
+ this.manager = manager;
274
+ this.onChange = onChange;
275
+ this.options = options;
276
+ this.globPatterns = globs.map((pattern) => normalizeChangePattern(pattern));
277
+ this.pathScopes = options?.pathScope?.length ? options.pathScope.map((pattern) => normalizeChangePattern(pattern)) : null;
278
+ this.shouldCoalesce = (options?.coalesce ?? "fire-once") !== "none";
279
+ this.coalesceMs = Math.max(0, Math.floor(options?.coalesceMs ?? DEFAULT_CHANGE_COALESCE_MS));
280
+ }
281
+ push(event) {
282
+ if (!this.active || !shouldPublishFilesystemEvent(event) || !this.matches(event.path)) {
283
+ return;
284
+ }
285
+ if (!this.shouldCoalesce) {
286
+ this.dispatch(event);
287
+ return;
288
+ }
289
+ const existing = this.pendingByPath.get(event.path);
290
+ if (existing) {
291
+ clearTimeout(existing.timer);
292
+ }
293
+ const timer = setTimeout(() => {
294
+ this.pendingByPath.delete(event.path);
295
+ if (!this.active) {
296
+ return;
297
+ }
298
+ this.dispatch(event);
299
+ }, this.coalesceMs);
300
+ this.pendingByPath.set(event.path, { event, timer });
301
+ }
302
+ async close() {
303
+ this.active = false;
304
+ for (const pending of this.pendingByPath.values()) {
305
+ clearTimeout(pending.timer);
306
+ }
307
+ this.pendingByPath.clear();
308
+ const drain = Promise.allSettled(Array.from(this.inFlight));
309
+ const drainMs = this.options?.drainMs;
310
+ if (typeof drainMs === "number" && Number.isFinite(drainMs) && drainMs >= 0) {
311
+ await Promise.race([
312
+ drain,
313
+ new Promise((resolve) => {
314
+ setTimeout(resolve, drainMs);
315
+ })
316
+ ]);
317
+ return;
318
+ }
319
+ await drain;
320
+ }
321
+ matches(path) {
322
+ const pathSegments = normalizeChangePath(path);
323
+ const matchesGlob = this.globPatterns.some((pattern) => matchChangeSegments(pattern, pathSegments));
324
+ if (!matchesGlob) {
325
+ return false;
326
+ }
327
+ if (!this.pathScopes) {
328
+ return true;
329
+ }
330
+ return this.pathScopes.some((pattern) => matchChangeSegments(pattern, pathSegments));
331
+ }
332
+ dispatch(event) {
333
+ const task = this.manager.materialize(event)
334
+ .then((changeEvent) => {
335
+ if (!this.active || !changeEvent) {
336
+ return;
337
+ }
338
+ return Promise.resolve(this.onChange(changeEvent));
339
+ })
340
+ .catch((error) => {
341
+ if (typeof console !== "undefined" && typeof console.error === "function") {
342
+ console.error("RelayFile subscribe handler failed", error);
343
+ }
344
+ })
345
+ .finally(() => {
346
+ this.inFlight.delete(task);
347
+ });
348
+ this.inFlight.add(task);
349
+ }
350
+ }
351
+ class RelayFileChangeStreamManager {
352
+ client;
353
+ workspaceId;
354
+ token;
355
+ baseUrl;
356
+ subscriptions = new Set();
357
+ openHandleCount = 0;
358
+ sync;
359
+ readyResolved = false;
360
+ readyInternal;
361
+ resolveReady;
362
+ rejectReady;
363
+ constructor(client, workspaceId, token, baseUrl) {
364
+ this.client = client;
365
+ this.workspaceId = workspaceId;
366
+ this.token = token;
367
+ this.baseUrl = baseUrl;
368
+ this.readyInternal = new Promise((resolve, reject) => {
369
+ this.resolveReady = resolve;
370
+ this.rejectReady = reject;
371
+ });
372
+ }
373
+ get ready() {
374
+ return this.readyInternal;
375
+ }
376
+ addSubscription(globs, onChange, options) {
377
+ const subscription = new RelayFileChangeSubscription(this, globs, onChange, options);
378
+ this.subscriptions.add(subscription);
379
+ this.ensureStarted();
380
+ return {
381
+ unsubscribe: async () => {
382
+ this.subscriptions.delete(subscription);
383
+ await subscription.close();
384
+ await this.maybeStop();
385
+ }
386
+ };
387
+ }
388
+ open() {
389
+ this.openHandleCount += 1;
390
+ this.ensureStarted();
391
+ return {
392
+ ready: this.ready,
393
+ unsubscribe: async () => {
394
+ this.openHandleCount = Math.max(0, this.openHandleCount - 1);
395
+ await this.maybeStop();
396
+ }
397
+ };
398
+ }
399
+ async materialize(event) {
400
+ if (!shouldPublishFilesystemEvent(event)) {
401
+ return null;
402
+ }
403
+ const eventId = normalizeChangeEventId(event, this.workspaceId);
404
+ const cache = getChangeLogCache(this.client, this.workspaceId);
405
+ const cached = cache.get(eventId);
406
+ if (cached) {
407
+ return toChangeEvent(this.client, cached, { workspaceId: this.workspaceId, token: this.token });
408
+ }
409
+ const hydrations = getPendingChangeHydrations(this.client, this.workspaceId);
410
+ const pending = hydrations.get(eventId);
411
+ if (pending) {
412
+ const record = await pending;
413
+ return record ? toChangeEvent(this.client, record, { workspaceId: this.workspaceId, token: this.token }) : null;
414
+ }
415
+ const inFlight = materializeChangeRecord(this.client, this.workspaceId, event, this.token);
416
+ hydrations.set(eventId, inFlight);
417
+ let record;
418
+ try {
419
+ record = await inFlight;
420
+ }
421
+ finally {
422
+ if (hydrations.get(eventId) === inFlight) {
423
+ hydrations.delete(eventId);
424
+ }
425
+ }
426
+ return record ? toChangeEvent(this.client, record, { workspaceId: this.workspaceId, token: this.token }) : null;
427
+ }
428
+ ensureStarted() {
429
+ if (this.sync) {
430
+ return;
431
+ }
432
+ const sync = new RelayFileSync({
433
+ client: this.client,
434
+ workspaceId: this.workspaceId,
435
+ baseUrl: this.baseUrl,
436
+ token: this.token,
437
+ onPollingFallback: () => {
438
+ this.resolveReadyOnce();
439
+ }
440
+ });
441
+ sync.on("open", () => {
442
+ this.resolveReadyOnce();
443
+ });
444
+ sync.on("state", (state) => {
445
+ if (state === "polling") {
446
+ this.resolveReadyOnce();
447
+ }
448
+ });
449
+ sync.on("error", (error) => {
450
+ if (!this.readyResolved) {
451
+ this.rejectReady(error);
452
+ }
453
+ });
454
+ sync.on("event", (event) => {
455
+ for (const subscription of this.subscriptions) {
456
+ subscription.push(event);
457
+ }
458
+ });
459
+ this.sync = sync;
460
+ sync.start();
461
+ if (sync.getState() === "polling") {
462
+ this.resolveReadyOnce();
463
+ }
464
+ }
465
+ resolveReadyOnce() {
466
+ if (this.readyResolved) {
467
+ return;
468
+ }
469
+ this.readyResolved = true;
470
+ this.resolveReady();
471
+ }
472
+ async maybeStop() {
473
+ if (this.openHandleCount > 0 || this.subscriptions.size > 0) {
474
+ return;
475
+ }
476
+ if (this.sync) {
477
+ const sync = this.sync;
478
+ this.sync = undefined;
479
+ await sync.stop();
480
+ }
481
+ const managers = changeStreamManagers.get(this.client);
482
+ if (!managers) {
483
+ return;
484
+ }
485
+ for (const [key, manager] of managers.entries()) {
486
+ if (manager === this) {
487
+ managers.delete(key);
488
+ }
489
+ }
490
+ }
491
+ }
492
+ function getStreamManager(client, workspaceId, token, baseUrl) {
493
+ let managers = changeStreamManagers.get(client);
494
+ if (!managers) {
495
+ managers = new Map();
496
+ changeStreamManagers.set(client, managers);
497
+ }
498
+ const key = `${workspaceId}:${token ?? CLIENT_TOKEN_STREAM_KEY}`;
499
+ const existing = managers.get(key);
500
+ if (existing) {
501
+ return existing;
502
+ }
503
+ const manager = new RelayFileChangeStreamManager(client, workspaceId, token, baseUrl);
504
+ managers.set(key, manager);
505
+ return manager;
506
+ }
507
+ function getChangeLogCache(client, workspaceId) {
508
+ let workspaceCaches = changeLogCaches.get(client);
509
+ if (!workspaceCaches) {
510
+ workspaceCaches = new Map();
511
+ changeLogCaches.set(client, workspaceCaches);
512
+ }
513
+ const existing = workspaceCaches.get(workspaceId);
514
+ if (existing) {
515
+ return existing;
516
+ }
517
+ const settings = changeLogSettings.get(client) ?? normalizeChangeLogOptions();
518
+ const cache = new WorkspaceChangeLogCache(settings.retentionMs, settings.maxEntries);
519
+ workspaceCaches.set(workspaceId, cache);
520
+ return cache;
521
+ }
522
+ function getPendingChangeHydrations(client, workspaceId) {
523
+ let workspaceHydrations = pendingChangeHydrations.get(client);
524
+ if (!workspaceHydrations) {
525
+ workspaceHydrations = new Map();
526
+ pendingChangeHydrations.set(client, workspaceHydrations);
527
+ }
528
+ const existing = workspaceHydrations.get(workspaceId);
529
+ if (existing) {
530
+ return existing;
531
+ }
532
+ const hydrations = new Map();
533
+ workspaceHydrations.set(workspaceId, hydrations);
534
+ return hydrations;
535
+ }
536
+ function mergeChangeRecords(records) {
537
+ const deduped = new Map();
538
+ for (const record of records) {
539
+ deduped.set(record.wire.id, record);
540
+ }
541
+ return Array.from(deduped.values()).sort((left, right) => {
542
+ const leftOccurredAt = Date.parse(left.wire.occurredAt);
543
+ const rightOccurredAt = Date.parse(right.wire.occurredAt);
544
+ if (leftOccurredAt !== rightOccurredAt) {
545
+ return leftOccurredAt - rightOccurredAt;
546
+ }
547
+ return left.storedAt - right.storedAt;
548
+ });
549
+ }
550
+ function normalizeChangePattern(pattern) {
551
+ if (typeof pattern !== "string" || pattern.length === 0) {
552
+ throw new Error("subscribe globs must be non-empty strings.");
553
+ }
554
+ if (!pattern.startsWith("/")) {
555
+ throw new Error("subscribe globs must start with '/'.");
556
+ }
557
+ if (pattern.includes("//")) {
558
+ throw new Error("subscribe globs cannot contain empty path segments.");
559
+ }
560
+ const segments = normalizeChangePath(pattern);
561
+ const recursiveIndex = segments.indexOf("**");
562
+ if (recursiveIndex >= 0 && recursiveIndex !== segments.length - 1) {
563
+ throw new Error("subscribe globs only support '**' as the trailing segment.");
564
+ }
565
+ return segments;
566
+ }
567
+ function normalizeChangePath(path) {
568
+ const normalized = path.startsWith("/") ? path : `/${path}`;
569
+ const trimmed = normalized.replace(/\/+$/, "");
570
+ if (trimmed === "") {
571
+ return [];
572
+ }
573
+ return trimmed.split("/").filter(Boolean);
574
+ }
575
+ function matchChangeSegments(pattern, path) {
576
+ if (pattern.length > 0 && pattern[pattern.length - 1] === "**") {
577
+ const prefix = pattern.slice(0, -1);
578
+ return path.length >= prefix.length && prefix.every((segment, index) => segment === "*" || segment === path[index]);
579
+ }
580
+ if (pattern.length !== path.length) {
581
+ return false;
582
+ }
583
+ return pattern.every((segment, index) => segment === "*" || segment === path[index]);
584
+ }
585
+ function shouldPublishFilesystemEvent(event) {
586
+ return event.type === "file.created" || event.type === "file.updated" || event.type === "file.deleted";
587
+ }
588
+ function decodeBase64Url(value) {
589
+ const normalized = value.replace(/-/g, "+").replace(/_/g, "/");
590
+ const padded = normalized.padEnd(Math.ceil(normalized.length / 4) * 4, "=");
591
+ if (typeof atob === "function") {
592
+ const decoded = atob(padded);
593
+ return decodeURIComponent(Array.from(decoded).map((char) => `%${char.charCodeAt(0).toString(16).padStart(2, "0")}`).join(""));
594
+ }
595
+ const bufferCtor = globalThis.Buffer;
596
+ if (bufferCtor) {
597
+ return bufferCtor.from(padded, "base64").toString("utf8");
598
+ }
599
+ throw new Error("No base64 decoder is available in this environment.");
600
+ }
601
+ function getWorkspaceIdFromToken(token) {
602
+ const parts = token.split(".");
603
+ if (parts.length < 2) {
604
+ return undefined;
605
+ }
606
+ try {
607
+ const parsed = JSON.parse(decodeBase64Url(parts[1] ?? ""));
608
+ return typeof parsed.workspace_id === "string" && parsed.workspace_id.length > 0 ? parsed.workspace_id : undefined;
609
+ }
610
+ catch {
611
+ return undefined;
612
+ }
613
+ }
614
+ function getAgentIdFromToken(token) {
615
+ const parts = token.split(".");
616
+ if (parts.length < 2) {
617
+ return undefined;
618
+ }
619
+ try {
620
+ const parsed = JSON.parse(decodeBase64Url(parts[1] ?? ""));
621
+ return typeof parsed.agent_name === "string" && parsed.agent_name.length > 0 ? parsed.agent_name : undefined;
622
+ }
623
+ catch {
624
+ return undefined;
625
+ }
626
+ }
627
+ function getSingleKnownWorkspaceId(client) {
628
+ const workspaceIds = new Set();
629
+ for (const registry of [changeStreamManagers.get(client), changeLogCaches.get(client)]) {
630
+ if (!registry) {
631
+ continue;
632
+ }
633
+ for (const key of registry.keys()) {
634
+ workspaceIds.add(key.split(":")[0] ?? key);
635
+ }
636
+ }
637
+ return workspaceIds.size === 1 ? Array.from(workspaceIds)[0] : undefined;
638
+ }
639
+ function inferProviderFromPath(path) {
640
+ return normalizeChangePath(path)[0] ?? "relayfile";
641
+ }
642
+ function singularizeSegment(segment) {
643
+ return segment.endsWith("s") && segment.length > 1 ? segment.slice(0, -1) : segment;
644
+ }
645
+ function stripExtension(value) {
646
+ return value.replace(/\.[^/.]+$/, "");
647
+ }
648
+ function inferResourceMetadata(path, data) {
649
+ const segments = normalizeChangePath(path);
650
+ const provider = readStringField(data, ["provider"]) ?? inferProviderFromPath(path);
651
+ const kind = readStringField(data, ["kind", "resourceType"])
652
+ ?? readStringField(data, ["type"])
653
+ ?? `${provider}.${singularizeSegment(segments[1] ?? "resource")}`;
654
+ const id = readStringField(data, ["id", "resourceId", "key"])
655
+ ?? stripExtension(segments[segments.length - 1] ?? path);
656
+ return {
657
+ path,
658
+ kind,
659
+ id,
660
+ provider
661
+ };
662
+ }
663
+ function readStringField(data, keys) {
664
+ if (!data || typeof data !== "object" || Array.isArray(data)) {
665
+ return undefined;
666
+ }
667
+ const record = data;
668
+ for (const key of keys) {
669
+ const value = record[key];
670
+ if (typeof value === "string" && value.trim().length > 0) {
671
+ return value.trim();
672
+ }
673
+ }
674
+ return undefined;
675
+ }
676
+ function readStringArrayField(data, keys) {
677
+ if (!data || typeof data !== "object" || Array.isArray(data)) {
678
+ return undefined;
679
+ }
680
+ const record = data;
681
+ for (const key of keys) {
682
+ const value = record[key];
683
+ if (Array.isArray(value)) {
684
+ const items = value.filter((entry) => typeof entry === "string" && entry.trim().length > 0).map((entry) => entry.trim());
685
+ if (items.length > 0) {
686
+ return items;
687
+ }
688
+ }
689
+ }
690
+ return undefined;
691
+ }
692
+ function readActorField(data) {
693
+ if (!data || typeof data !== "object" || Array.isArray(data)) {
694
+ return undefined;
695
+ }
696
+ const record = data;
697
+ const rawActor = record.actor ?? record.assignee ?? record.author;
698
+ if (typeof rawActor === "string" && rawActor.trim().length > 0) {
699
+ return { id: rawActor.trim() };
700
+ }
701
+ if (!rawActor || typeof rawActor !== "object" || Array.isArray(rawActor)) {
702
+ return undefined;
703
+ }
704
+ const actorRecord = rawActor;
705
+ const id = typeof actorRecord.id === "string" ? actorRecord.id.trim() : undefined;
706
+ if (!id) {
707
+ return undefined;
708
+ }
709
+ const displayName = typeof actorRecord.displayName === "string"
710
+ ? actorRecord.displayName.trim()
711
+ : typeof actorRecord.name === "string"
712
+ ? actorRecord.name.trim()
713
+ : undefined;
714
+ return { id, ...(displayName ? { displayName } : {}) };
715
+ }
716
+ function truncateString(value, maxLength) {
717
+ return value.length <= maxLength ? value : `${value.slice(0, Math.max(0, maxLength - 3))}...`;
718
+ }
719
+ function basename(path) {
720
+ const parts = normalizeChangePath(path);
721
+ return parts[parts.length - 1] ?? path;
722
+ }
723
+ function buildChangeSummary(path, data) {
724
+ const title = readStringField(data, ["title", "name", "summary", "subject"]) ?? stripExtension(basename(path));
725
+ const summary = {
726
+ title: truncateString(title, 120)
727
+ };
728
+ const status = readStringField(data, ["status", "state"]);
729
+ if (status) {
730
+ summary.status = status;
731
+ }
732
+ const priority = readStringField(data, ["priority"]);
733
+ if (priority) {
734
+ summary.priority = priority;
735
+ }
736
+ const labels = readStringArrayField(data, ["labels"]);
737
+ if (labels) {
738
+ summary.labels = labels.slice(0, 8);
739
+ }
740
+ const actor = readActorField(data);
741
+ if (actor) {
742
+ summary.actor = actor;
743
+ }
744
+ const fieldsChanged = readStringArrayField(data, ["fieldsChanged", "changedFields"]);
745
+ if (fieldsChanged) {
746
+ summary.fieldsChanged = fieldsChanged;
747
+ }
748
+ const tags = readStringArrayField(data, ["tags"]);
749
+ if (tags) {
750
+ summary.tags = tags.slice(0, 8);
751
+ }
752
+ return summary;
753
+ }
754
+ function decodeFilePayload(file) {
755
+ if (file.encoding === "base64") {
756
+ return {
757
+ contentBase64: file.content,
758
+ contentType: file.contentType,
759
+ encoding: "base64"
760
+ };
761
+ }
762
+ const looksJson = file.contentType.includes("json") || file.path.endsWith(".json");
763
+ if (looksJson) {
764
+ try {
765
+ return JSON.parse(file.content);
766
+ }
767
+ catch {
768
+ // Fall through to the raw string when payloads are malformed.
769
+ }
770
+ }
771
+ return file.content;
772
+ }
773
+ async function sha256Hex(value) {
774
+ let subtle = globalThis.crypto?.subtle;
775
+ if (!subtle) {
776
+ try {
777
+ const nodeCrypto = await import("node:crypto");
778
+ subtle = nodeCrypto.webcrypto?.subtle;
779
+ if (!subtle) {
780
+ return `sha256:${nodeCrypto.createHash("sha256").update(value).digest("hex")}`;
781
+ }
782
+ }
783
+ catch {
784
+ // Fall through to the non-content fallback below.
785
+ }
786
+ }
787
+ if (!subtle) {
788
+ const nonce = globalThis.crypto?.randomUUID?.() ?? `unavailable-${Date.now().toString(36)}`;
789
+ return `sha256:unavailable:${nonce}`;
790
+ }
791
+ const hash = await subtle.digest("SHA-256", new TextEncoder().encode(value));
792
+ const bytes = new Uint8Array(hash);
793
+ return `sha256:${Array.from(bytes).map((byte) => byte.toString(16).padStart(2, "0")).join("")}`;
794
+ }
795
+ function normalizeChangeEventId(event, workspaceId) {
796
+ if (event.eventId && !event.eventId.startsWith("ws:")) {
797
+ return event.eventId;
798
+ }
799
+ return [
800
+ "relayfile",
801
+ workspaceId,
802
+ event.type,
803
+ event.path,
804
+ event.revision,
805
+ event.timestamp
806
+ ].join(":");
807
+ }
808
+ function toChangeEvent(client, record, contextOverride) {
809
+ const context = contextOverride ?? record.context ?? { workspaceId: record.wire.workspace };
810
+ return {
811
+ ...record.wire,
812
+ expand: async (level) => {
813
+ const normalizedLevel = (level ?? "summary");
814
+ if (normalizedLevel === "summary") {
815
+ return {
816
+ level: normalizedLevel,
817
+ path: record.wire.resource.path,
818
+ summary: record.wire.summary
819
+ };
820
+ }
821
+ if (normalizedLevel === "full") {
822
+ const resource = await client.getResourceAtEvent(record.wire.id, context);
823
+ return {
824
+ level: normalizedLevel,
825
+ path: resource.path,
826
+ data: resource.data
827
+ };
828
+ }
829
+ throw createM2NotImplementedError(`ChangeEvent.expand(${JSON.stringify(normalizedLevel)})`);
830
+ }
831
+ };
832
+ }
833
+ function normalizeWireChangeEvent(payload) {
834
+ const data = (payload ?? {});
835
+ const resource = (data.resource ?? {});
836
+ const summary = (data.summary ?? {});
837
+ return {
838
+ id: typeof data.id === "string" ? data.id : "",
839
+ workspace: typeof data.workspace === "string" ? data.workspace : "",
840
+ agentId: typeof data.agentId === "string" ? data.agentId : undefined,
841
+ type: "relayfile.changed",
842
+ occurredAt: typeof data.occurredAt === "string" ? data.occurredAt : new Date().toISOString(),
843
+ resource: {
844
+ path: typeof resource.path === "string" ? resource.path : "",
845
+ kind: typeof resource.kind === "string" ? resource.kind : "relayfile.resource",
846
+ id: typeof resource.id === "string" ? resource.id : "",
847
+ provider: typeof resource.provider === "string" ? resource.provider : "relayfile"
848
+ },
849
+ summary: {
850
+ ...(typeof summary.title === "string" ? { title: summary.title } : {}),
851
+ ...(typeof summary.status === "string" ? { status: summary.status } : {}),
852
+ ...(typeof summary.priority === "string" ? { priority: summary.priority } : {}),
853
+ ...(Array.isArray(summary.labels) ? { labels: summary.labels.filter((entry) => typeof entry === "string") } : {}),
854
+ ...(summary.actor && typeof summary.actor === "object"
855
+ ? {
856
+ actor: {
857
+ id: typeof summary.actor.id === "string" ? summary.actor.id : "",
858
+ ...(typeof summary.actor.displayName === "string"
859
+ ? { displayName: summary.actor.displayName }
860
+ : {})
861
+ }
862
+ }
863
+ : {}),
864
+ ...(Array.isArray(summary.fieldsChanged)
865
+ ? { fieldsChanged: summary.fieldsChanged.filter((entry) => typeof entry === "string") }
866
+ : {}),
867
+ ...(Array.isArray(summary.tags) ? { tags: summary.tags.filter((entry) => typeof entry === "string").slice(0, 8) } : {})
868
+ },
869
+ ...(typeof data.digest === "string" ? { digest: data.digest } : {})
870
+ };
871
+ }
872
+ async function materializeChangeRecord(client, workspaceId, event, token) {
873
+ const eventId = normalizeChangeEventId(event, workspaceId);
874
+ const occurredAt = event.timestamp || new Date().toISOString();
875
+ const fallbackDigest = event.revision ? `revision:${event.revision}` : `deleted:${occurredAt}`;
876
+ const fallbackResource = {
877
+ path: event.path,
878
+ data: { path: event.path, deleted: event.type === "file.deleted" },
879
+ digest: fallbackDigest
880
+ };
881
+ let tokenAgentId;
882
+ try {
883
+ tokenAgentId = getAgentIdFromToken(token ?? await client.getToken());
884
+ }
885
+ catch {
886
+ tokenAgentId = undefined;
887
+ }
888
+ let resource = fallbackResource;
889
+ let wire = {
890
+ id: eventId,
891
+ workspace: workspaceId,
892
+ ...(tokenAgentId ? { agentId: tokenAgentId } : {}),
893
+ type: "relayfile.changed",
894
+ occurredAt,
895
+ resource: inferResourceMetadata(event.path, resource.data),
896
+ summary: buildChangeSummary(event.path, resource.data),
897
+ digest: resource.digest
898
+ };
899
+ if (event.type !== "file.deleted") {
900
+ try {
901
+ const file = await client.readFile({ workspaceId, path: event.path, token });
902
+ const data = decodeFilePayload(file);
903
+ const digest = await sha256Hex(file.content);
904
+ resource = {
905
+ path: event.path,
906
+ data,
907
+ digest
908
+ };
909
+ wire = {
910
+ ...wire,
911
+ resource: inferResourceMetadata(event.path, data),
912
+ summary: buildChangeSummary(event.path, data),
913
+ digest
914
+ };
915
+ }
916
+ catch (error) {
917
+ if (typeof console !== "undefined" && typeof console.warn === "function") {
918
+ console.warn(`[relayfile-sdk] Failed to materialize change event for ${event.path}; falling back to path-only summary.`, error);
919
+ }
920
+ }
921
+ }
922
+ const record = {
923
+ wire,
924
+ resource,
925
+ storedAt: Date.now(),
926
+ context: { workspaceId, token }
927
+ };
928
+ getChangeLogCache(client, workspaceId).record(record);
929
+ return record;
930
+ }
161
931
  export class RelayFileClient {
162
932
  baseUrl;
163
933
  tokenProvider;
@@ -170,6 +940,7 @@ export class RelayFileClient {
170
940
  this.fetchImpl = options.fetchImpl ?? fetch.bind(globalThis);
171
941
  this.userAgent = options.userAgent;
172
942
  this.retryOptions = normalizeRetryOptions(options.retry);
943
+ changeLogSettings.set(this, normalizeChangeLogOptions(options.changeLog));
173
944
  }
174
945
  /**
175
946
  * Resolve the current access token via the configured token provider.
@@ -223,7 +994,8 @@ export class RelayFileClient {
223
994
  method: "GET",
224
995
  path: `/v1/workspaces/${encodeURIComponent(input.workspaceId)}/fs/file${query}`,
225
996
  correlationId: input.correlationId,
226
- signal: input.signal
997
+ signal: input.signal,
998
+ tokenOverride: input.token
227
999
  });
228
1000
  }
229
1001
  async queryFiles(workspaceId, options = {}) {
@@ -350,6 +1122,115 @@ export class RelayFileClient {
350
1122
  signal: options.signal
351
1123
  });
352
1124
  }
1125
+ subscribe(globs, onChange, options) {
1126
+ const setup = this.resolveWorkspaceId(options?.aclToken)
1127
+ .then((workspaceId) => {
1128
+ const manager = getStreamManager(this, workspaceId, options?.aclToken, this.baseUrl);
1129
+ return manager.addSubscription(globs, onChange, options);
1130
+ });
1131
+ return {
1132
+ async unsubscribe() {
1133
+ const subscription = await setup;
1134
+ await subscription.unsubscribe();
1135
+ },
1136
+ };
1137
+ }
1138
+ open(options) {
1139
+ const manager = getStreamManager(this, options.workspaceId, options.aclToken, this.baseUrl);
1140
+ const connection = manager.open();
1141
+ const replay = this.primeReplayCache(options).catch((error) => {
1142
+ if (typeof console !== "undefined" && typeof console.error === "function") {
1143
+ console.error("RelayFile change-stream replay initialization failed", error);
1144
+ }
1145
+ });
1146
+ return {
1147
+ ready: Promise.all([connection.ready, replay]).then(() => undefined),
1148
+ unsubscribe: () => connection.unsubscribe()
1149
+ };
1150
+ }
1151
+ async getResourceAtEvent(eventId, context) {
1152
+ const workspaceId = context?.workspaceId ?? await this.resolveWorkspaceId(context?.token);
1153
+ const effectiveContext = context ?? { workspaceId };
1154
+ const cache = getChangeLogCache(this, workspaceId);
1155
+ const cached = cache.get(eventId);
1156
+ if (cached && cached.resource.data !== null) {
1157
+ return cached.resource;
1158
+ }
1159
+ const pending = getPendingChangeHydrations(this, workspaceId).get(eventId);
1160
+ if (pending) {
1161
+ try {
1162
+ await pending;
1163
+ }
1164
+ catch {
1165
+ // Fall through to the retained lookup endpoint if live hydration fails.
1166
+ }
1167
+ const hydrated = cache.get(eventId);
1168
+ if (hydrated && hydrated.resource.data !== null) {
1169
+ return hydrated.resource;
1170
+ }
1171
+ }
1172
+ return this.request({
1173
+ method: "GET",
1174
+ path: `/v1/workspaces/${encodeURIComponent(workspaceId)}/fs/changes/resource${buildQuery({ eventId })}`,
1175
+ tokenOverride: effectiveContext.token
1176
+ });
1177
+ }
1178
+ async listChangesSince(isoTimestamp, context) {
1179
+ const workspaceId = context?.workspaceId ?? await this.resolveWorkspaceId(context?.token);
1180
+ const effectiveContext = context ?? { workspaceId };
1181
+ const cache = getChangeLogCache(this, workspaceId);
1182
+ const cached = cache.listSince(isoTimestamp);
1183
+ let records = cached;
1184
+ try {
1185
+ const payload = await this.request({
1186
+ method: "GET",
1187
+ path: `/v1/workspaces/${encodeURIComponent(workspaceId)}/fs/changes${buildQuery({ since: isoTimestamp })}`,
1188
+ tokenOverride: effectiveContext.token
1189
+ });
1190
+ records = mergeChangeRecords([
1191
+ ...cached,
1192
+ ...(payload.events ?? []).map((event) => this.cacheWireChangeEvent(workspaceId, normalizeWireChangeEvent(event), effectiveContext))
1193
+ ]);
1194
+ }
1195
+ catch (error) {
1196
+ if (cached.length === 0) {
1197
+ throw error;
1198
+ }
1199
+ }
1200
+ return {
1201
+ events: records.map((record) => toChangeEvent(this, record, context))
1202
+ };
1203
+ }
1204
+ async listLastNChanges(limit, context) {
1205
+ const safeLimit = Math.max(0, Math.floor(limit));
1206
+ if (safeLimit === 0) {
1207
+ return { events: [] };
1208
+ }
1209
+ const workspaceId = context?.workspaceId ?? await this.resolveWorkspaceId(context?.token);
1210
+ const effectiveContext = context ?? { workspaceId };
1211
+ const cache = getChangeLogCache(this, workspaceId);
1212
+ const cached = cache.listLastN(safeLimit);
1213
+ let records = cached;
1214
+ try {
1215
+ const payload = await this.request({
1216
+ method: "GET",
1217
+ path: `/v1/workspaces/${encodeURIComponent(workspaceId)}/fs/changes${buildQuery({ last: safeLimit })}`,
1218
+ tokenOverride: effectiveContext.token
1219
+ });
1220
+ records = mergeChangeRecords([
1221
+ ...cached,
1222
+ ...(payload.events ?? []).map((event) => this.cacheWireChangeEvent(workspaceId, normalizeWireChangeEvent(event), effectiveContext))
1223
+ ]).slice(-safeLimit);
1224
+ }
1225
+ catch (error) {
1226
+ if (cached.length === 0) {
1227
+ throw error;
1228
+ }
1229
+ }
1230
+ return {
1231
+ events: records.map((record) => toChangeEvent(this, record, context))
1232
+ };
1233
+ }
353
1234
  async exportWorkspace(options) {
354
1235
  const format = options.format ?? "json";
355
1236
  const query = buildQuery({ format });
@@ -586,6 +1467,44 @@ export class RelayFileClient {
586
1467
  signal: input.signal
587
1468
  });
588
1469
  }
1470
+ cacheWireChangeEvent(workspaceId, wire, context) {
1471
+ const record = {
1472
+ wire,
1473
+ resource: {
1474
+ path: wire.resource.path,
1475
+ data: null,
1476
+ digest: wire.digest ?? `event:${wire.id}`
1477
+ },
1478
+ storedAt: Date.now(),
1479
+ context: context ?? { workspaceId }
1480
+ };
1481
+ return getChangeLogCache(this, workspaceId).record(record);
1482
+ }
1483
+ async primeReplayCache(options) {
1484
+ if (!options.replayOnStart || options.replayOnStart === "none") {
1485
+ return;
1486
+ }
1487
+ if (options.replayOnStart.startsWith("since:")) {
1488
+ await this.listChangesSince(options.replayOnStart.slice("since:".length), { workspaceId: options.workspaceId, token: options.aclToken });
1489
+ return;
1490
+ }
1491
+ if (options.replayOnStart.startsWith("last:")) {
1492
+ const limit = Number.parseInt(options.replayOnStart.slice("last:".length), 10);
1493
+ await this.listLastNChanges(limit, { workspaceId: options.workspaceId, token: options.aclToken });
1494
+ }
1495
+ }
1496
+ async resolveWorkspaceId(tokenOverride) {
1497
+ const knownWorkspaceId = getSingleKnownWorkspaceId(this);
1498
+ if (knownWorkspaceId) {
1499
+ return knownWorkspaceId;
1500
+ }
1501
+ const token = tokenOverride ?? await this.getToken();
1502
+ const workspaceId = getWorkspaceIdFromToken(token);
1503
+ if (workspaceId) {
1504
+ return workspaceId;
1505
+ }
1506
+ throw new Error("RelayFile proactive-runtime APIs require a workspace-scoped JWT with a workspace_id claim.");
1507
+ }
589
1508
  async request(params) {
590
1509
  const response = await this.performRequest(params);
591
1510
  const payload = await this.readPayload(response);
@@ -612,7 +1531,7 @@ export class RelayFileClient {
612
1531
  let retries = 0;
613
1532
  const url = `${this.baseUrl}${params.path}`;
614
1533
  for (;;) {
615
- const token = await resolveToken(this.tokenProvider);
1534
+ const token = params.tokenOverride ?? await resolveToken(this.tokenProvider);
616
1535
  const headers = {
617
1536
  Authorization: `Bearer ${token}`,
618
1537
  ...baseHeaders
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { RelayFileClient, DEFAULT_RELAYFILE_BASE_URL, type AccessTokenProvider, type ConnectWebSocketOptions, type RelayFileClientOptions, type RelayFileRetryOptions, type WebSocketConnection } from "./client.js";
1
+ export { RelayFileClient, DEFAULT_RELAYFILE_BASE_URL, type AccessTokenProvider, type RelayFileChangeLogOptions, type ConnectWebSocketOptions, type RelayFileClientOptions, type RelayFileRetryOptions, type WebSocketConnection } from "./client.js";
2
2
  export { RelayfileSetup, RELAYFILE_SDK_VERSION, WorkspaceHandle } from "./setup.js";
3
3
  export { type RelayfileCloudLoginOptions, type RelayfileCloudTokenSet, type RelayfileCloudTokenSetupOptions } from "./cloud-login.js";
4
4
  export { CloudAbortError, CloudApiError, CloudTimeoutError, InvalidLocalDirError, InvalidMountModeError, InvalidRemotePathError, IntegrationConnectionTimeoutError, MalformedCloudResponseError, MissingConnectionIdError, MountModeUnavailableError, MountReadyTimeoutError, MountSessionInputError, ProviderNotConnectedError, ProviderNotReadyError, RelayfileSetupError, UnknownProviderError } from "./setup-errors.js";
@@ -10,7 +10,7 @@ export { InvalidStateError, PayloadTooLargeError, QueueFullError, RelayFileApiEr
10
10
  export { IntegrationProvider, computeCanonicalPath } from "./provider.js";
11
11
  export type { WebhookInput, ListProviderFilesOptions, WatchProviderEventsOptions } from "./provider.js";
12
12
  export type { ConnectionProvider, NormalizedWebhook, ProxyHeaders, ProxyMethod, ProxyQuery, ProxyRequest, ProxyResponse, } from "./connection.js";
13
- export type { AckResponse, AckWritebackInput, AckWritebackResponse, AdminIngressAlert, AdminIngressAlertProfile, AdminIngressEffectiveAlertProfile, AdminIngressAlertSeverity, AdminIngressAlertThresholds, AdminIngressAlertTotals, AdminIngressAlertType, AdminIngressStatusResponse, AdminSyncAlert, AdminSyncAlertSeverity, AdminSyncAlertThresholds, AdminSyncAlertTotals, AdminSyncAlertType, AdminSyncStatusResponse, BackendStatusResponse, BulkWriteFile, BulkWriteInput, BulkWriteResponse, CommitForkInput, CommitForkResponse, ConflictErrorResponse, CreateForkInput, ContentIdentity, DeleteFileInput, DeadLetterFeedResponse, DeadLetterItem, DiscardForkInput, ErrorResponse, EventFeedResponse, ExportFormat, ExportJsonResponse, ExportOptions, FileQueryItem, FileQueryResponse, FileReadResponse, FileSemantics, FileWriteRequest, FilesystemEvent, FilesystemEventType, EventOrigin, GetEventsOptions, GetAdminSyncStatusOptions, GetAdminIngressStatusOptions, GetOperationsOptions, GetSyncDeadLettersOptions, GetSyncIngressStatusOptions, GetSyncStatusOptions, IngestWebhookInput, ListTreeOptions, OperationFeedResponse, OperationStatus, OperationStatusResponse, QueuedResponse, QueryFilesOptions, ReadFileInput, RelayFileJwtClaims, SyncIngressStatusResponse, SyncProviderStatus, SyncProviderStatusState, SyncRefreshRequest, SyncStatusResponse, TreeEntry, TreeResponse, WritebackActionType, WritebackState, WritebackItem, WriteFileInput, WriteQueuedResponse } from "./types.js";
13
+ export type { AckResponse, AckWritebackInput, AckWritebackResponse, AdminIngressAlert, AdminIngressAlertProfile, AdminIngressEffectiveAlertProfile, AdminIngressAlertSeverity, AdminIngressAlertThresholds, AdminIngressAlertTotals, AdminIngressAlertType, AdminIngressStatusResponse, AdminSyncAlert, AdminSyncAlertSeverity, AdminSyncAlertThresholds, AdminSyncAlertTotals, AdminSyncAlertType, AdminSyncStatusResponse, BackendStatusResponse, BulkWriteFile, BulkWriteInput, BulkWriteResponse, ChangeLogQueryResult, ChangeEvent, ChangeEventActor, ChangeEventResource, ChangeEventSummary, ChangeStreamConnection, ChangeStreamConnectionOptions, CommitForkInput, CommitForkResponse, ConflictErrorResponse, CreateForkInput, ContentIdentity, DeleteFileInput, DeadLetterFeedResponse, DeadLetterItem, DiscardForkInput, ErrorResponse, EventSummary, EventFeedResponse, ExportFormat, ExportJsonResponse, ExportOptions, FileQueryItem, FileQueryResponse, FileReadResponse, FileSemantics, FileWriteRequest, FilesystemEvent, FilesystemEventType, EventOrigin, Expansion, ExpansionLevel, GetEventsOptions, GetAdminSyncStatusOptions, GetAdminIngressStatusOptions, GetOperationsOptions, GetSyncDeadLettersOptions, GetSyncIngressStatusOptions, GetSyncStatusOptions, IngestWebhookInput, ListTreeOptions, OperationFeedResponse, OperationStatus, OperationStatusResponse, QueuedResponse, QueryFilesOptions, ReadFileInput, ReplayOptions, ResourceAtEventResult, SummaryExpansion, FullExpansion, DiffExpansion, ThreadExpansion, RelayFileJwtClaims, SubscribeOptions, Subscription, SyncIngressStatusResponse, SyncProviderStatus, SyncProviderStatusState, SyncRefreshRequest, SyncStatusResponse, TreeEntry, TreeResponse, WritebackActionType, WritebackState, WritebackItem, WriteFileInput, WriteQueuedResponse } from "./types.js";
14
14
  export type { ForkHandle, ForkOptions } from "@relayfile/core";
15
15
  export type { WriteEvent, WriteEventActor, WriteEventOperation, WriteEventSource } from "@relayfile/core";
16
16
  export { WritebackConsumer } from "./writeback-consumer.js";
package/dist/types.d.ts CHANGED
@@ -136,6 +136,106 @@ export interface FilesystemEvent {
136
136
  correlationId?: string;
137
137
  timestamp: string;
138
138
  }
139
+ export interface ChangeEventResource {
140
+ path: string;
141
+ kind: string;
142
+ id: string;
143
+ provider: string;
144
+ }
145
+ export interface ChangeEventActor {
146
+ id: string;
147
+ displayName?: string;
148
+ }
149
+ export interface ChangeEventSummary {
150
+ title?: string;
151
+ status?: string;
152
+ priority?: string;
153
+ labels?: string[];
154
+ actor?: ChangeEventActor;
155
+ fieldsChanged?: string[];
156
+ /** Optional compact tag list. Producers must cap this at 8 entries. */
157
+ tags?: string[];
158
+ }
159
+ /**
160
+ * Canonical lightweight event summary shape exported for proactive-runtime
161
+ * adapters. `buildSummary(payload)` should return this exact structure.
162
+ */
163
+ export type EventSummary = ChangeEventSummary;
164
+ export type ExpansionLevel = "summary" | "full" | "diff" | "thread";
165
+ export interface SummaryExpansion {
166
+ level: "summary";
167
+ path: string;
168
+ summary: ChangeEventSummary;
169
+ }
170
+ export interface FullExpansion<TData = unknown> {
171
+ level: "full";
172
+ path: string;
173
+ data: TData;
174
+ }
175
+ export interface DiffExpansion {
176
+ level: "diff";
177
+ path: string;
178
+ diff: Record<string, unknown>;
179
+ }
180
+ export interface ThreadExpansion {
181
+ level: "thread";
182
+ path: string;
183
+ thread: Record<string, unknown>;
184
+ }
185
+ export type Expansion<L extends ExpansionLevel = ExpansionLevel> = L extends "summary" ? SummaryExpansion : L extends "full" ? FullExpansion : L extends "diff" ? DiffExpansion : ThreadExpansion;
186
+ /**
187
+ * Proactive runtime relayfile notification envelope.
188
+ *
189
+ * This differs from the lower-level FilesystemEvent feed above. The proactive
190
+ * runtime consumes a small, stable notification that points at the canonical
191
+ * payload in VFS instead of inlining provider payloads directly.
192
+ *
193
+ * M1 exposes the type so downstream packages can compile against the final
194
+ * shape before M2 wires live delivery.
195
+ */
196
+ export interface ChangeEvent {
197
+ id: string;
198
+ workspace: string;
199
+ agentId?: string;
200
+ type: "relayfile.changed";
201
+ occurredAt: string;
202
+ resource: ChangeEventResource;
203
+ summary: ChangeEventSummary;
204
+ expand<L extends ExpansionLevel = "summary">(level?: L): Promise<Expansion<L>>;
205
+ digest?: string;
206
+ }
207
+ export interface SubscribeOptions {
208
+ coalesce?: "none" | "fire-once";
209
+ coalesceMs?: number;
210
+ pathScope?: string[];
211
+ aclToken?: string;
212
+ drainMs?: number;
213
+ }
214
+ export interface Subscription {
215
+ unsubscribe(): Promise<void>;
216
+ }
217
+ export type ReplayOptions = {
218
+ replayOnStart?: "none";
219
+ } | {
220
+ replayOnStart: `since:${string}`;
221
+ } | {
222
+ replayOnStart: `last:${number}`;
223
+ };
224
+ export type ChangeStreamConnectionOptions = ReplayOptions & {
225
+ workspaceId: string;
226
+ aclToken?: string;
227
+ };
228
+ export interface ChangeStreamConnection extends Subscription {
229
+ readonly ready: Promise<void>;
230
+ }
231
+ export interface ResourceAtEventResult {
232
+ path: string;
233
+ data: unknown;
234
+ digest: string;
235
+ }
236
+ export interface ChangeLogQueryResult {
237
+ events: ChangeEvent[];
238
+ }
139
239
  export interface EventFeedResponse {
140
240
  events: FilesystemEvent[];
141
241
  nextCursor: string | null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@relayfile/sdk",
3
- "version": "0.7.10",
3
+ "version": "0.7.12",
4
4
  "description": "TypeScript SDK for relayfile — real-time filesystem for humans and agents",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -22,7 +22,7 @@
22
22
  "prepublishOnly": "npm run build"
23
23
  },
24
24
  "dependencies": {
25
- "@relayfile/core": "0.7.10"
25
+ "@relayfile/core": "0.7.12"
26
26
  },
27
27
  "devDependencies": {
28
28
  "typescript": "^5.7.3",