@relayfile/sdk 0.7.10 → 0.7.11
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 +7 -1
- package/dist/client.d.ts +26 -1
- package/dist/client.js +921 -2
- package/dist/index.d.ts +2 -2
- package/dist/types.d.ts +100 -0
- package/package.json +2 -2
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.
|
|
3
|
+
"version": "0.7.11",
|
|
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.
|
|
25
|
+
"@relayfile/core": "0.7.11"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"typescript": "^5.7.3",
|