@relayfile/sdk 0.6.3 → 0.6.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/onWrite.d.ts +24 -0
- package/dist/onWrite.js +268 -0
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -4,12 +4,14 @@ export { type RelayfileCloudLoginOptions, type RelayfileCloudTokenSet, type Rela
|
|
|
4
4
|
export { CloudAbortError, CloudApiError, CloudTimeoutError, IntegrationConnectionTimeoutError, MalformedCloudResponseError, MissingConnectionIdError, RelayfileSetupError, UnknownProviderError } from "./setup-errors.js";
|
|
5
5
|
export { WORKSPACE_INTEGRATION_PROVIDERS, type AgentWorkspaceInvite, type AgentWorkspaceInviteOptions, type ConnectIntegrationOptions, type ConnectIntegrationResult, type CreateWorkspaceOptions, type JoinWorkspaceOptions, type RelayfileSetupOptions, type RelayfileSetupRetryOptions, type WaitForConnectionOptions, type WorkspaceInfo, type WorkspaceIntegrationProvider, type WorkspaceMountEnv, type WorkspaceMountEnvOptions, type WorkspacePermissions } from "./setup-types.js";
|
|
6
6
|
export { RelayFileSync, type RelayFileSyncOptions, type RelayFileSyncPong, type RelayFileSyncReconnectOptions, type RelayFileSyncSocket, type RelayFileSyncState } from "./sync.js";
|
|
7
|
+
export { onWrite, pathMatches, type OnWriteClient, type OnWriteHandler, type OnWriteHandlerError, type OnWriteOptions } from "./onWrite.js";
|
|
7
8
|
export { InvalidStateError, PayloadTooLargeError, QueueFullError, RelayFileApiError, RevisionConflictError } from "./errors.js";
|
|
8
9
|
export { IntegrationProvider, computeCanonicalPath } from "./provider.js";
|
|
9
10
|
export type { WebhookInput, ListProviderFilesOptions, WatchProviderEventsOptions } from "./provider.js";
|
|
10
11
|
export type { ConnectionProvider, NormalizedWebhook, ProxyHeaders, ProxyMethod, ProxyQuery, ProxyRequest, ProxyResponse, } from "./connection.js";
|
|
11
12
|
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";
|
|
12
13
|
export type { ForkHandle, ForkOptions } from "@relayfile/core";
|
|
14
|
+
export type { WriteEvent, WriteEventActor, WriteEventOperation, WriteEventSource } from "@relayfile/core";
|
|
13
15
|
export { WritebackConsumer } from "./writeback-consumer.js";
|
|
14
16
|
export type { WritebackHandler, WritebackConsumerOptions } from "./writeback-consumer.js";
|
|
15
17
|
export * from "./integration-adapter.js";
|
package/dist/index.js
CHANGED
|
@@ -3,6 +3,7 @@ export { RelayfileSetup, RELAYFILE_SDK_VERSION, WorkspaceHandle } from "./setup.
|
|
|
3
3
|
export { CloudAbortError, CloudApiError, CloudTimeoutError, IntegrationConnectionTimeoutError, MalformedCloudResponseError, MissingConnectionIdError, RelayfileSetupError, UnknownProviderError } from "./setup-errors.js";
|
|
4
4
|
export { WORKSPACE_INTEGRATION_PROVIDERS } from "./setup-types.js";
|
|
5
5
|
export { RelayFileSync } from "./sync.js";
|
|
6
|
+
export { onWrite, pathMatches } from "./onWrite.js";
|
|
6
7
|
export { InvalidStateError, PayloadTooLargeError, QueueFullError, RelayFileApiError, RevisionConflictError } from "./errors.js";
|
|
7
8
|
// Integration providers
|
|
8
9
|
export { IntegrationProvider, computeCanonicalPath } from "./provider.js";
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { WriteEvent, WriteEventOperation } from "@relayfile/core";
|
|
2
|
+
import { RelayFileClient } from "./client.js";
|
|
3
|
+
import { type RelayFileSyncSocket } from "./sync.js";
|
|
4
|
+
export type OnWriteHandler = (event: WriteEvent) => void | Promise<void>;
|
|
5
|
+
export interface OnWriteHandlerError {
|
|
6
|
+
pattern: string;
|
|
7
|
+
path: string;
|
|
8
|
+
error: unknown;
|
|
9
|
+
retryable: false;
|
|
10
|
+
}
|
|
11
|
+
export type OnWriteClient = RelayFileClient & {
|
|
12
|
+
recordHandlerError?(error: OnWriteHandlerError): void | Promise<void>;
|
|
13
|
+
};
|
|
14
|
+
export interface OnWriteOptions {
|
|
15
|
+
client?: OnWriteClient;
|
|
16
|
+
workspaceId?: string;
|
|
17
|
+
operations?: WriteEventOperation[];
|
|
18
|
+
signal?: AbortSignal;
|
|
19
|
+
baseUrl?: string;
|
|
20
|
+
token?: string;
|
|
21
|
+
webSocketFactory?: (url: string) => RelayFileSyncSocket;
|
|
22
|
+
}
|
|
23
|
+
export declare function pathMatches(pattern: string, path: string): boolean;
|
|
24
|
+
export declare function onWrite(pattern: string, handler: OnWriteHandler, options?: OnWriteOptions): () => void;
|
package/dist/onWrite.js
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import { RelayFileClient, DEFAULT_RELAYFILE_BASE_URL } from "./client.js";
|
|
2
|
+
import { RelayFileSync } from "./sync.js";
|
|
3
|
+
const DEFAULT_OPERATIONS = ["create", "update"];
|
|
4
|
+
const DEFAULT_RECONNECT_MIN_DELAY_MS = 1000;
|
|
5
|
+
const DEFAULT_RECONNECT_MAX_DELAY_MS = 30000;
|
|
6
|
+
const dispatchers = new WeakMap();
|
|
7
|
+
let nextRegistrationId = 1;
|
|
8
|
+
let defaultClient;
|
|
9
|
+
export function pathMatches(pattern, path) {
|
|
10
|
+
const patternSegments = normalizePattern(pattern);
|
|
11
|
+
const pathSegments = normalizePath(path);
|
|
12
|
+
return matchSegments(patternSegments, pathSegments);
|
|
13
|
+
}
|
|
14
|
+
export function onWrite(pattern, handler, options = {}) {
|
|
15
|
+
const normalizedPattern = `/${normalizePattern(pattern).join("/")}`;
|
|
16
|
+
if (typeof handler !== "function") {
|
|
17
|
+
throw new Error("onWrite handler must be a function.");
|
|
18
|
+
}
|
|
19
|
+
const client = options.client ?? getDefaultClient();
|
|
20
|
+
const workspaceId = options.workspaceId ?? readEnv("RELAYFILE_WORKSPACE_ID");
|
|
21
|
+
if (!workspaceId) {
|
|
22
|
+
throw new Error("onWrite requires options.workspaceId or RELAYFILE_WORKSPACE_ID.");
|
|
23
|
+
}
|
|
24
|
+
const operations = new Set(options.operations ?? DEFAULT_OPERATIONS);
|
|
25
|
+
for (const operation of operations) {
|
|
26
|
+
if (operation !== "create" && operation !== "update" && operation !== "delete") {
|
|
27
|
+
throw new Error(`Invalid onWrite operation: ${operation}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
// The dispatcher cache is keyed by client; a single shared WebSocket is
|
|
31
|
+
// bound to one workspace. v1 scopes a client to a single workspace per the
|
|
32
|
+
// design doc (Out-of-scope: "Cross-workspace subscriptions"). Reject
|
|
33
|
+
// mismatched workspaceId rather than silently attaching to the wrong feed.
|
|
34
|
+
let dispatcher = dispatchers.get(client);
|
|
35
|
+
if (dispatcher && dispatcher.workspaceId !== workspaceId) {
|
|
36
|
+
throw new Error(`onWrite registrations on the same client must use the same workspaceId. Existing="${dispatcher.workspaceId}", new="${workspaceId}". Construct a separate RelayFileClient per workspace.`);
|
|
37
|
+
}
|
|
38
|
+
if (!dispatcher) {
|
|
39
|
+
dispatcher = new OnWriteDispatcher(client, workspaceId);
|
|
40
|
+
dispatchers.set(client, dispatcher);
|
|
41
|
+
}
|
|
42
|
+
const registration = {
|
|
43
|
+
id: nextRegistrationId++,
|
|
44
|
+
pattern: normalizedPattern,
|
|
45
|
+
operations,
|
|
46
|
+
handler
|
|
47
|
+
};
|
|
48
|
+
dispatcher.register(registration, {
|
|
49
|
+
workspaceId,
|
|
50
|
+
signal: options.signal,
|
|
51
|
+
baseUrl: options.baseUrl,
|
|
52
|
+
token: options.token,
|
|
53
|
+
webSocketFactory: options.webSocketFactory
|
|
54
|
+
});
|
|
55
|
+
return () => {
|
|
56
|
+
dispatcher?.unregister(registration.id);
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
class OnWriteDispatcher {
|
|
60
|
+
client;
|
|
61
|
+
// Captured at construction. RelayFileSync normalizes the FilesystemEvent
|
|
62
|
+
// shape and does not surface workspaceId on emitted events, so we stamp the
|
|
63
|
+
// subscribed workspaceId onto every WriteEvent we hand to user handlers.
|
|
64
|
+
// It also gates registrations: see the cross-workspace check in onWrite().
|
|
65
|
+
workspaceId;
|
|
66
|
+
registrations = [];
|
|
67
|
+
patternChains = new Map();
|
|
68
|
+
sync;
|
|
69
|
+
constructor(client, workspaceId) {
|
|
70
|
+
this.client = client;
|
|
71
|
+
this.workspaceId = workspaceId;
|
|
72
|
+
}
|
|
73
|
+
register(registration, options) {
|
|
74
|
+
this.registrations.push(registration);
|
|
75
|
+
if (options.signal) {
|
|
76
|
+
if (options.signal.aborted) {
|
|
77
|
+
this.unregister(registration.id);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
options.signal.addEventListener("abort", () => this.unregister(registration.id), { once: true });
|
|
81
|
+
}
|
|
82
|
+
this.ensureSync(options);
|
|
83
|
+
}
|
|
84
|
+
unregister(id) {
|
|
85
|
+
const index = this.registrations.findIndex((registration) => registration.id === id);
|
|
86
|
+
if (index >= 0) {
|
|
87
|
+
this.registrations.splice(index, 1);
|
|
88
|
+
}
|
|
89
|
+
if (this.registrations.length === 0 && this.sync) {
|
|
90
|
+
void this.sync.stop();
|
|
91
|
+
this.sync = undefined;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
ensureSync(options) {
|
|
95
|
+
if (this.sync) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
this.sync = RelayFileSync.connect({
|
|
99
|
+
client: this.client,
|
|
100
|
+
workspaceId: options.workspaceId,
|
|
101
|
+
baseUrl: options.baseUrl ?? readEnv("RELAYFILE_BASE_URL") ?? DEFAULT_RELAYFILE_BASE_URL,
|
|
102
|
+
token: options.token ?? readEnv("RELAYFILE_TOKEN"),
|
|
103
|
+
reconnect: {
|
|
104
|
+
minDelayMs: DEFAULT_RECONNECT_MIN_DELAY_MS,
|
|
105
|
+
maxDelayMs: DEFAULT_RECONNECT_MAX_DELAY_MS
|
|
106
|
+
},
|
|
107
|
+
webSocketFactory: options.webSocketFactory,
|
|
108
|
+
onEvent: (event) => {
|
|
109
|
+
void this.dispatch(event);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
async dispatch(event) {
|
|
114
|
+
const writeEvent = toWriteEvent(event, this.workspaceId);
|
|
115
|
+
if (!writeEvent) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
for (const registration of [...this.registrations]) {
|
|
119
|
+
if (!registration.operations.has(writeEvent.operation) || !pathMatches(registration.pattern, writeEvent.path)) {
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
const previous = this.patternChains.get(registration.pattern) ?? Promise.resolve();
|
|
123
|
+
const next = previous
|
|
124
|
+
.catch(() => undefined)
|
|
125
|
+
.then(() => this.runHandler(registration, writeEvent));
|
|
126
|
+
this.patternChains.set(registration.pattern, next);
|
|
127
|
+
void next.finally(() => {
|
|
128
|
+
if (this.patternChains.get(registration.pattern) === next) {
|
|
129
|
+
this.patternChains.delete(registration.pattern);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
async runHandler(registration, event) {
|
|
135
|
+
try {
|
|
136
|
+
await registration.handler(event);
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
await this.recordHandlerError({
|
|
140
|
+
pattern: registration.pattern,
|
|
141
|
+
path: event.path,
|
|
142
|
+
error,
|
|
143
|
+
retryable: false
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
// The "handler errors do not propagate" guarantee covers the recorder too:
|
|
148
|
+
// if the customer's recordHandlerError implementation throws or rejects, fall
|
|
149
|
+
// back to console.error rather than letting the rejection bubble up the
|
|
150
|
+
// dispatch chain (which would skip subsequent handlers for the same pattern).
|
|
151
|
+
async recordHandlerError(error) {
|
|
152
|
+
if (typeof this.client.recordHandlerError === "function") {
|
|
153
|
+
try {
|
|
154
|
+
await this.client.recordHandlerError(error);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
catch (reportingError) {
|
|
158
|
+
if (typeof console !== "undefined" && typeof console.error === "function") {
|
|
159
|
+
console.error("Relayfile onWrite handler-error reporter failed", reportingError);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
if (typeof console !== "undefined" && typeof console.error === "function") {
|
|
164
|
+
console.error("Relayfile onWrite handler error", error);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
function normalizePattern(pattern) {
|
|
169
|
+
if (typeof pattern !== "string" || pattern.length === 0) {
|
|
170
|
+
throw new Error("onWrite pattern must be a non-empty string.");
|
|
171
|
+
}
|
|
172
|
+
if (!pattern.startsWith("/")) {
|
|
173
|
+
throw new Error("onWrite pattern must start with '/'.");
|
|
174
|
+
}
|
|
175
|
+
if (pattern.includes("//")) {
|
|
176
|
+
throw new Error("onWrite pattern cannot contain empty path segments.");
|
|
177
|
+
}
|
|
178
|
+
const segments = normalizePath(pattern);
|
|
179
|
+
const recursiveIndex = segments.indexOf("**");
|
|
180
|
+
if (recursiveIndex >= 0 && recursiveIndex !== segments.length - 1) {
|
|
181
|
+
throw new Error("onWrite pattern only supports '**' as the trailing segment.");
|
|
182
|
+
}
|
|
183
|
+
return segments;
|
|
184
|
+
}
|
|
185
|
+
function normalizePath(path) {
|
|
186
|
+
if (!path.startsWith("/")) {
|
|
187
|
+
return normalizePath(`/${path}`);
|
|
188
|
+
}
|
|
189
|
+
const trimmed = path.replace(/\/+$/, "");
|
|
190
|
+
if (trimmed === "") {
|
|
191
|
+
return [];
|
|
192
|
+
}
|
|
193
|
+
return trimmed.split("/").filter(Boolean);
|
|
194
|
+
}
|
|
195
|
+
// Trailing `**` matches **zero or more** trailing segments — same as gitignore
|
|
196
|
+
// and standard glob conventions, and what the design doc specifies ("any
|
|
197
|
+
// number of segments"). `/linear/issues/**` therefore matches both
|
|
198
|
+
// `/linear/issues` (the collection root) and `/linear/issues/PROJ-1/comments`.
|
|
199
|
+
// `*` matches exactly one segment; `**` is only valid as the last segment.
|
|
200
|
+
function matchSegments(pattern, path) {
|
|
201
|
+
if (pattern.length > 0 && pattern[pattern.length - 1] === "**") {
|
|
202
|
+
const prefix = pattern.slice(0, -1);
|
|
203
|
+
return path.length >= prefix.length && prefix.every((segment, index) => segment === "*" || segment === path[index]);
|
|
204
|
+
}
|
|
205
|
+
if (pattern.length !== path.length) {
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
return pattern.every((segment, index) => segment === "*" || segment === path[index]);
|
|
209
|
+
}
|
|
210
|
+
function toWriteEvent(event, workspaceId) {
|
|
211
|
+
const operation = operationFromEventType(event.type);
|
|
212
|
+
if (!operation) {
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
// RelayFileSync currently surfaces only:
|
|
216
|
+
// eventId, type, path, revision, origin, provider, correlationId, timestamp
|
|
217
|
+
// Fields the wider WriteEvent contract advertises but the wire format does
|
|
218
|
+
// not yet preserve — previousRevision, value, actor — are intentionally
|
|
219
|
+
// omitted/null here. Wiring them through the wire format and
|
|
220
|
+
// normalizeFilesystemEvent is a follow-up; v1 callers should treat
|
|
221
|
+
// previousRevision/value/actor as not-yet-populated.
|
|
222
|
+
return {
|
|
223
|
+
workspaceId: workspaceId ?? "",
|
|
224
|
+
path: event.path,
|
|
225
|
+
operation,
|
|
226
|
+
revision: event.revision,
|
|
227
|
+
previousRevision: null,
|
|
228
|
+
timestamp: event.timestamp,
|
|
229
|
+
source: sourceFromOrigin(event.origin)
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
function operationFromEventType(type) {
|
|
233
|
+
if (type === "file.created") {
|
|
234
|
+
return "create";
|
|
235
|
+
}
|
|
236
|
+
if (type === "file.updated") {
|
|
237
|
+
return "update";
|
|
238
|
+
}
|
|
239
|
+
if (type === "file.deleted") {
|
|
240
|
+
return "delete";
|
|
241
|
+
}
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
function sourceFromOrigin(origin) {
|
|
245
|
+
if (origin === "agent_write") {
|
|
246
|
+
return "agent";
|
|
247
|
+
}
|
|
248
|
+
if (origin === "provider_sync") {
|
|
249
|
+
return "sync";
|
|
250
|
+
}
|
|
251
|
+
return "api";
|
|
252
|
+
}
|
|
253
|
+
function getDefaultClient() {
|
|
254
|
+
if (!defaultClient) {
|
|
255
|
+
const token = readEnv("RELAYFILE_TOKEN");
|
|
256
|
+
if (!token) {
|
|
257
|
+
throw new Error("onWrite requires options.client or RELAYFILE_TOKEN.");
|
|
258
|
+
}
|
|
259
|
+
defaultClient = new RelayFileClient({
|
|
260
|
+
baseUrl: readEnv("RELAYFILE_BASE_URL") ?? DEFAULT_RELAYFILE_BASE_URL,
|
|
261
|
+
token
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
return defaultClient;
|
|
265
|
+
}
|
|
266
|
+
function readEnv(name) {
|
|
267
|
+
return globalThis.process?.env?.[name];
|
|
268
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@relayfile/sdk",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.5",
|
|
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",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"prepublishOnly": "npm run build"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@relayfile/core": "0.6.
|
|
23
|
+
"@relayfile/core": "0.6.5"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
26
|
"typescript": "^5.7.3",
|