@relayfile/sdk 0.1.5 → 0.1.6
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/writeback-consumer.d.ts +31 -0
- package/dist/writeback-consumer.js +139 -0
- package/package.json +2 -1
package/dist/index.d.ts
CHANGED
|
@@ -5,3 +5,5 @@ export { IntegrationProvider, computeCanonicalPath } from "./provider.js";
|
|
|
5
5
|
export type { WebhookInput, ListProviderFilesOptions, WatchProviderEventsOptions } from "./provider.js";
|
|
6
6
|
export type { ConnectionProvider, NormalizedWebhook, ProxyHeaders, ProxyMethod, ProxyQuery, ProxyRequest, ProxyResponse, } from "./connection.js";
|
|
7
7
|
export type { AckResponse, AckWritebackInput, AckWritebackResponse, AdminIngressAlert, AdminIngressAlertProfile, AdminIngressEffectiveAlertProfile, AdminIngressAlertSeverity, AdminIngressAlertThresholds, AdminIngressAlertTotals, AdminIngressAlertType, AdminIngressStatusResponse, AdminSyncAlert, AdminSyncAlertSeverity, AdminSyncAlertThresholds, AdminSyncAlertTotals, AdminSyncAlertType, AdminSyncStatusResponse, BackendStatusResponse, BulkWriteFile, BulkWriteInput, BulkWriteResponse, ConflictErrorResponse, DeleteFileInput, DeadLetterFeedResponse, DeadLetterItem, 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, RelayFileJwtClaims, SyncIngressStatusResponse, SyncProviderStatus, SyncProviderStatusState, SyncRefreshRequest, SyncStatusResponse, TreeEntry, TreeResponse, WritebackActionType, WritebackState, WritebackItem, WriteFileInput, WriteQueuedResponse } from "./types.js";
|
|
8
|
+
export { WritebackConsumer } from "./writeback-consumer.js";
|
|
9
|
+
export type { WritebackHandler, WritebackConsumerOptions } from "./writeback-consumer.js";
|
package/dist/index.js
CHANGED
|
@@ -3,3 +3,4 @@ export { RelayFileSync } from "./sync.js";
|
|
|
3
3
|
export { InvalidStateError, PayloadTooLargeError, QueueFullError, RelayFileApiError, RevisionConflictError } from "./errors.js";
|
|
4
4
|
// Integration providers
|
|
5
5
|
export { IntegrationProvider, computeCanonicalPath } from "./provider.js";
|
|
6
|
+
export { WritebackConsumer } from "./writeback-consumer.js";
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { RelayFileClient } from "./client.js";
|
|
2
|
+
import type { ConnectionProvider } from "./connection.js";
|
|
3
|
+
import type { WritebackItem } from "./types.js";
|
|
4
|
+
export interface WritebackHandler {
|
|
5
|
+
canHandle(path: string): boolean;
|
|
6
|
+
execute(item: WritebackItem, provider: ConnectionProvider): Promise<void>;
|
|
7
|
+
}
|
|
8
|
+
export interface WritebackConsumerOptions {
|
|
9
|
+
client: RelayFileClient;
|
|
10
|
+
workspaceId: string;
|
|
11
|
+
handlers: WritebackHandler[];
|
|
12
|
+
provider: ConnectionProvider;
|
|
13
|
+
pollIntervalMs?: number;
|
|
14
|
+
signal?: AbortSignal;
|
|
15
|
+
}
|
|
16
|
+
export declare class WritebackConsumer {
|
|
17
|
+
private readonly client;
|
|
18
|
+
private readonly workspaceId;
|
|
19
|
+
private readonly handlers;
|
|
20
|
+
private readonly provider;
|
|
21
|
+
private readonly pollIntervalMs;
|
|
22
|
+
private readonly signal?;
|
|
23
|
+
private loopPromise?;
|
|
24
|
+
private stopped;
|
|
25
|
+
constructor(opts: WritebackConsumerOptions);
|
|
26
|
+
start(): Promise<void>;
|
|
27
|
+
stop(): void;
|
|
28
|
+
pollOnce(signal?: AbortSignal): Promise<void>;
|
|
29
|
+
private runLoop;
|
|
30
|
+
private ackFailure;
|
|
31
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
const DEFAULT_POLL_INTERVAL_MS = 1_000;
|
|
2
|
+
export class WritebackConsumer {
|
|
3
|
+
client;
|
|
4
|
+
workspaceId;
|
|
5
|
+
handlers;
|
|
6
|
+
provider;
|
|
7
|
+
pollIntervalMs;
|
|
8
|
+
signal;
|
|
9
|
+
loopPromise;
|
|
10
|
+
stopped = false;
|
|
11
|
+
constructor(opts) {
|
|
12
|
+
this.client = opts.client;
|
|
13
|
+
this.workspaceId = opts.workspaceId;
|
|
14
|
+
this.handlers = opts.handlers;
|
|
15
|
+
this.provider = opts.provider;
|
|
16
|
+
this.pollIntervalMs = opts.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
17
|
+
this.signal = opts.signal;
|
|
18
|
+
if (this.pollIntervalMs < 0) {
|
|
19
|
+
throw new RangeError("pollIntervalMs must be greater than or equal to 0");
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
async start() {
|
|
23
|
+
if (this.loopPromise) {
|
|
24
|
+
if (this.stopped) {
|
|
25
|
+
// Previous run was stopped — reset and start fresh
|
|
26
|
+
this.stopped = false;
|
|
27
|
+
this.loopPromise = undefined;
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
return this.loopPromise;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
this.loopPromise = this.runLoop();
|
|
34
|
+
return this.loopPromise;
|
|
35
|
+
}
|
|
36
|
+
stop() {
|
|
37
|
+
this.stopped = true;
|
|
38
|
+
}
|
|
39
|
+
async pollOnce(signal = this.signal) {
|
|
40
|
+
const items = await this.client.listPendingWritebacks(this.workspaceId, undefined, signal);
|
|
41
|
+
for (const item of items) {
|
|
42
|
+
if (signal?.aborted || this.stopped) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const handler = this.handlers.find((candidate) => candidate.canHandle(item.path));
|
|
46
|
+
if (!handler) {
|
|
47
|
+
await this.ackFailure(item, new Error(`No writeback handler found for path: ${item.path}`), signal);
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
let executed = false;
|
|
51
|
+
try {
|
|
52
|
+
await handler.execute(item, this.provider);
|
|
53
|
+
executed = true;
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
if (isAbortError(error) || signal?.aborted) {
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
await this.ackFailure(item, error, signal);
|
|
60
|
+
}
|
|
61
|
+
if (executed) {
|
|
62
|
+
try {
|
|
63
|
+
await this.client.ackWriteback({
|
|
64
|
+
workspaceId: this.workspaceId,
|
|
65
|
+
itemId: item.id,
|
|
66
|
+
success: true,
|
|
67
|
+
correlationId: item.correlationId,
|
|
68
|
+
signal
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
catch (ackError) {
|
|
72
|
+
if (isAbortError(ackError) || signal?.aborted) {
|
|
73
|
+
throw ackError;
|
|
74
|
+
}
|
|
75
|
+
// Handler succeeded — do NOT send failure ack (would cause duplicate writes on retry).
|
|
76
|
+
// Log and move on; server will eventually time out and retry the ack.
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
async runLoop() {
|
|
82
|
+
while (!this.stopped && !this.signal?.aborted) {
|
|
83
|
+
try {
|
|
84
|
+
await this.pollOnce(this.signal);
|
|
85
|
+
if (this.stopped || this.signal?.aborted) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
await sleep(this.pollIntervalMs, this.signal);
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
if (isAbortError(error) || this.signal?.aborted || this.stopped) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
throw error;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
async ackFailure(item, error, signal) {
|
|
99
|
+
await this.client.ackWriteback({
|
|
100
|
+
workspaceId: this.workspaceId,
|
|
101
|
+
itemId: item.id,
|
|
102
|
+
success: false,
|
|
103
|
+
error: toErrorMessage(error),
|
|
104
|
+
correlationId: item.correlationId,
|
|
105
|
+
signal
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
async function sleep(ms, signal) {
|
|
110
|
+
if (ms === 0) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (signal?.aborted) {
|
|
114
|
+
throw signal.reason ?? new DOMException("The operation was aborted", "AbortError");
|
|
115
|
+
}
|
|
116
|
+
await new Promise((resolve, reject) => {
|
|
117
|
+
const onAbort = () => {
|
|
118
|
+
clearTimeout(timer);
|
|
119
|
+
reject(signal.reason ?? new DOMException("The operation was aborted", "AbortError"));
|
|
120
|
+
};
|
|
121
|
+
const timer = setTimeout(() => {
|
|
122
|
+
signal?.removeEventListener("abort", onAbort);
|
|
123
|
+
resolve();
|
|
124
|
+
}, ms);
|
|
125
|
+
if (!signal) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
function isAbortError(error) {
|
|
132
|
+
return error instanceof DOMException && error.name === "AbortError";
|
|
133
|
+
}
|
|
134
|
+
function toErrorMessage(error) {
|
|
135
|
+
if (error instanceof Error) {
|
|
136
|
+
return error.message;
|
|
137
|
+
}
|
|
138
|
+
return String(error);
|
|
139
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@relayfile/sdk",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
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",
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
],
|
|
11
11
|
"scripts": {
|
|
12
12
|
"build": "tsc",
|
|
13
|
+
"typecheck": "tsc --noEmit",
|
|
13
14
|
"test": "vitest run",
|
|
14
15
|
"prepublishOnly": "npm run build"
|
|
15
16
|
},
|