@hearthlog/runtime 0.0.0-stage → 0.1.1
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/LICENSE +235 -0
- package/package.json +51 -3
- package/src/access/session.ts +223 -0
- package/src/access/user-session.ts +61 -0
- package/src/ai/decisions.ts +89 -0
- package/src/calendar/draft.ts +84 -0
- package/src/calendar/feeds.ts +91 -0
- package/src/calendar/groups.ts +198 -0
- package/src/calendar/logic.ts +141 -0
- package/src/calendar/preferences.ts +40 -0
- package/src/calendar/weeks.ts +56 -0
- package/src/commands/index.ts +184 -0
- package/src/commands/local-commit.ts +214 -0
- package/src/commands/standard.ts +8 -0
- package/src/commands/validation.ts +206 -0
- package/src/db/bootstrap.ts +72 -0
- package/src/db/calendar.ts +638 -0
- package/src/db/dynamic-ddl.ts +127 -0
- package/src/db/entities.ts +208 -0
- package/src/db/history.ts +194 -0
- package/src/db/index.ts +1240 -0
- package/src/db/kv.ts +33 -0
- package/src/db/local-statistics.ts +106 -0
- package/src/db/module-projections.ts +46 -0
- package/src/db/raw-browser.ts +361 -0
- package/src/db/read-repositories.ts +29 -0
- package/src/db/read-session.ts +47 -0
- package/src/db/schema-version.ts +76 -0
- package/src/db/session-context.ts +33 -0
- package/src/event/code-export.ts +65 -0
- package/src/event/dynamic-entities.ts +503 -0
- package/src/event/dynamic-materializer.ts +288 -0
- package/src/event/handlers.ts +490 -0
- package/src/event/index.ts +53 -0
- package/src/event/meta-entity-types.ts +165 -0
- package/src/event/projection-builder.ts +384 -0
- package/src/event/queries.ts +126 -0
- package/src/event/rebuild.ts +176 -0
- package/src/event/reference-lookup.ts +93 -0
- package/src/event/registry.ts +158 -0
- package/src/event/schema-effects.ts +45 -0
- package/src/event/types.ts +20 -0
- package/src/history/action-boundary.ts +18 -0
- package/src/history/action-detail.ts +23 -0
- package/src/history/change-comparison.ts +29 -0
- package/src/history/change-display.ts +41 -0
- package/src/history/contracts.ts +63 -0
- package/src/history/history.ts +372 -0
- package/src/history/legacy-replay.ts +69 -0
- package/src/history/legacy-url.ts +9 -0
- package/src/history/lineage.ts +123 -0
- package/src/history/log-format.ts +17 -0
- package/src/history/log-map.ts +110 -0
- package/src/history/navigation.ts +14 -0
- package/src/history/point-snapshot.ts +229 -0
- package/src/history/timeline.ts +215 -0
- package/src/history/url.ts +18 -0
- package/src/ledger/README.md +118 -0
- package/src/ledger/admin-client.ts +616 -0
- package/src/ledger/client.ts +10 -0
- package/src/ledger/config.ts +117 -0
- package/src/ledger/entity-id.ts +115 -0
- package/src/ledger/errors.ts +144 -0
- package/src/ledger/http-client.ts +590 -0
- package/src/ledger/index.ts +67 -0
- package/src/ledger/socket.ts +400 -0
- package/src/ledger/types.ts +202 -0
- package/src/ledger/version-tracker-singleton.ts +12 -0
- package/src/ledger/version-tracker.ts +259 -0
- package/src/log-management.ts +123 -0
- package/src/log-validation.ts +19 -0
- package/src/native/reminders.ts +2 -0
- package/src/native/shell.svelte.ts +136 -0
- package/src/native/shell.ts +102 -0
- package/src/pwa/registration.ts +169 -0
- package/src/recovery/local-recovery.ts +59 -0
- package/src/recovery/reset-sequence.ts +13 -0
- package/src/runtime/clock.ts +43 -0
- package/src/runtime/kept-page-data.svelte.ts +42 -0
- package/src/runtime/page-data.svelte.ts +160 -0
- package/src/runtime/phone-header.svelte.ts +17 -0
- package/src/runtime/read-coordinator.ts +103 -0
- package/src/runtime/refresh.ts +15 -0
- package/src/runtime/start-page.ts +56 -0
- package/src/runtime/view.ts +83 -0
- package/src/runtime/workspace.ts +62 -0
- package/src/storage/indexeddb-statistics.ts +153 -0
- package/src/store/attention.svelte.ts +51 -0
- package/src/store/conflict-merge.ts +169 -0
- package/src/store/entity-sync-state.svelte.ts +36 -0
- package/src/store/event-log.svelte.ts +109 -0
- package/src/store/events.svelte.ts +14 -0
- package/src/store/local-context.ts +93 -0
- package/src/store/local-store.svelte.ts +58 -0
- package/src/store/log-version.svelte.ts +35 -0
- package/src/store/logs.svelte.ts +60 -0
- package/src/store/moved.svelte.ts +43 -0
- package/src/store/offline-readiness.svelte.ts +188 -0
- package/src/store/outbox-request.ts +61 -0
- package/src/store/outbox.ts +426 -0
- package/src/store/pending-work-transfer.ts +49 -0
- package/src/store/pending-work.ts +143 -0
- package/src/store/sync-coordinator.ts +170 -0
- package/src/store/sync-invalidation.ts +10 -0
- package/src/store/sync-issues.ts +406 -0
- package/src/store/sync-progress.ts +54 -0
- package/src/store/sync-state.ts +127 -0
- package/src/store/sync.svelte.ts +1010 -0
- package/src/store/tab-coordination.ts +164 -0
- package/src/testing/e2e-seed.ts +40 -0
- package/src/testing/local-database.ts +120 -0
- package/src/testing/projection-database.ts +105 -0
- package/src/testing/test-entities.ts +28 -0
- package/src/theme/appearance.svelte.ts +111 -0
- package/src/utils/address.ts +5 -0
- package/src/utils/async.ts +30 -0
- package/src/utils/long-press.ts +70 -0
- package/src/utils/perf-dataset.ts +405 -0
- package/src/utils/perf.ts +627 -0
- package/README.md +0 -3
|
@@ -0,0 +1,590 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP Client for the Ledger Service API
|
|
3
|
+
*
|
|
4
|
+
* HTTP transport with retry logic, request/response logging, realtime socket
|
|
5
|
+
* construction, and API error handling.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { EventSocket } from "./socket.js";
|
|
9
|
+
import type { LedgerConfig, RetryPolicy } from "./config.js";
|
|
10
|
+
import type {
|
|
11
|
+
LogSummary,
|
|
12
|
+
LogListResponse,
|
|
13
|
+
CreateLogRequest,
|
|
14
|
+
CreateLogResponse,
|
|
15
|
+
ForkLogRequest,
|
|
16
|
+
LogMetadata,
|
|
17
|
+
AppendEventsRequest,
|
|
18
|
+
AppendEventsResponse,
|
|
19
|
+
ConflictResponse,
|
|
20
|
+
EventBatch,
|
|
21
|
+
StoredEvent,
|
|
22
|
+
SessionResponse,
|
|
23
|
+
ErrorResponse,
|
|
24
|
+
EntityVersionChainErrorResponse,
|
|
25
|
+
} from "./types.js";
|
|
26
|
+
import {
|
|
27
|
+
LedgerServiceError,
|
|
28
|
+
VersionConflictError,
|
|
29
|
+
NotFoundError,
|
|
30
|
+
AuthenticationError,
|
|
31
|
+
EntityVersionChainError,
|
|
32
|
+
IdempotencyMismatchError,
|
|
33
|
+
isLedgerServiceError,
|
|
34
|
+
isVersionConflictError,
|
|
35
|
+
isNotFoundError,
|
|
36
|
+
isAuthenticationError,
|
|
37
|
+
} from "./errors.js";
|
|
38
|
+
import { fetchAccessProtected } from '../access/session.js';
|
|
39
|
+
|
|
40
|
+
// Re-export errors for convenience
|
|
41
|
+
export {
|
|
42
|
+
LedgerServiceError,
|
|
43
|
+
VersionConflictError,
|
|
44
|
+
NotFoundError,
|
|
45
|
+
AuthenticationError,
|
|
46
|
+
} from "./errors.js";
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Options for HTTP requests
|
|
50
|
+
*/
|
|
51
|
+
interface RequestOptions {
|
|
52
|
+
method: "GET" | "POST" | "PUT" | "DELETE";
|
|
53
|
+
path: string;
|
|
54
|
+
body?: unknown;
|
|
55
|
+
queryParams?: Record<string, string | number | undefined>;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Client for the Ledger Service API.
|
|
60
|
+
*/
|
|
61
|
+
export class LedgerClient {
|
|
62
|
+
private readonly config: LedgerConfig;
|
|
63
|
+
private readonly baseUrl: string;
|
|
64
|
+
|
|
65
|
+
constructor(config: LedgerConfig) {
|
|
66
|
+
this.config = config;
|
|
67
|
+
// Ensure baseUrl doesn't have trailing slash
|
|
68
|
+
this.baseUrl = config.baseUrl.replace(/\/$/, "");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Get the base URL for the client
|
|
73
|
+
*/
|
|
74
|
+
getBaseUrl(): string {
|
|
75
|
+
return this.baseUrl;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Get the auth token for the client
|
|
80
|
+
*/
|
|
81
|
+
getAuthToken(): string | undefined {
|
|
82
|
+
return this.config.authToken;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private supportsActionMetadata = false;
|
|
86
|
+
private capabilitiesChecked = false;
|
|
87
|
+
private supportsWorkspaceRestore = false;
|
|
88
|
+
private supportsEventExcision = false;
|
|
89
|
+
private sessionRequest: Promise<SessionResponse> | null = null;
|
|
90
|
+
|
|
91
|
+
async getWorkspaceRestoreSupport(): Promise<boolean> {
|
|
92
|
+
if (!this.capabilitiesChecked) await this.getSession();
|
|
93
|
+
return this.supportsWorkspaceRestore;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async getEventExcisionSupport(): Promise<boolean> {
|
|
97
|
+
if (!this.capabilitiesChecked) await this.getSession();
|
|
98
|
+
return this.supportsEventExcision;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private replayVersion(): number {
|
|
102
|
+
if (this.supportsEventExcision) return 2;
|
|
103
|
+
if (this.supportsWorkspaceRestore) return 1;
|
|
104
|
+
return 0;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async getActionMetadataSupport(): Promise<boolean> {
|
|
108
|
+
if (!this.capabilitiesChecked) await this.getSession();
|
|
109
|
+
return this.supportsActionMetadata;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async getSession(): Promise<SessionResponse> {
|
|
113
|
+
this.sessionRequest ??= this.request<SessionResponse>({ method: "GET", path: "/session" })
|
|
114
|
+
.then((session) => {
|
|
115
|
+
this.supportsActionMetadata = session.capabilities?.includes('action-metadata-v1') ?? false;
|
|
116
|
+
this.supportsWorkspaceRestore = session.capabilities?.includes('workspace-restore-v1') ?? false;
|
|
117
|
+
this.supportsEventExcision = session.capabilities?.includes('event-excision-v1') ?? false;
|
|
118
|
+
this.capabilitiesChecked = true;
|
|
119
|
+
return session;
|
|
120
|
+
})
|
|
121
|
+
.finally(() => { this.sessionRequest = null; });
|
|
122
|
+
return this.sessionRequest;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Makes an HTTP request with retry logic and error handling
|
|
127
|
+
*/
|
|
128
|
+
private async request<T>(options: RequestOptions): Promise<T> {
|
|
129
|
+
const { method, path, body, queryParams } = options;
|
|
130
|
+
|
|
131
|
+
// Build URL with query parameters
|
|
132
|
+
let url = `${this.baseUrl}${path}`;
|
|
133
|
+
if (queryParams) {
|
|
134
|
+
const params = new URLSearchParams();
|
|
135
|
+
Object.entries(queryParams).forEach(([key, value]) => {
|
|
136
|
+
if (value !== undefined) {
|
|
137
|
+
params.append(key, String(value));
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
const queryString = params.toString();
|
|
141
|
+
if (queryString) {
|
|
142
|
+
url += `?${queryString}`;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Build headers
|
|
147
|
+
const headers: Record<string, string> = {
|
|
148
|
+
"Content-Type": "application/json",
|
|
149
|
+
|
|
150
|
+
};
|
|
151
|
+
if (this.supportsWorkspaceRestore || this.supportsEventExcision) headers["X-Ledger-Replay-Version"] = String(this.replayVersion());
|
|
152
|
+
if (this.config.authToken) {
|
|
153
|
+
headers["Authorization"] = `Bearer ${this.config.authToken}`;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Log request
|
|
157
|
+
this.logRequest(method, url, body);
|
|
158
|
+
|
|
159
|
+
// Execute request with retry logic
|
|
160
|
+
return await this.executeWithRetry(async () => {
|
|
161
|
+
const controller = new AbortController();
|
|
162
|
+
const timeoutId = setTimeout(
|
|
163
|
+
() => controller.abort(),
|
|
164
|
+
this.config.timeout,
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
const response = await fetchAccessProtected(url, {
|
|
169
|
+
method,
|
|
170
|
+
headers,
|
|
171
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
172
|
+
signal: controller.signal,
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
clearTimeout(timeoutId);
|
|
176
|
+
|
|
177
|
+
// Handle response
|
|
178
|
+
return await this.handleResponse<T>(response);
|
|
179
|
+
} catch (error) {
|
|
180
|
+
clearTimeout(timeoutId);
|
|
181
|
+
|
|
182
|
+
// Handle abort/timeout
|
|
183
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
184
|
+
throw new LedgerServiceError(
|
|
185
|
+
`Request timeout after ${this.config.timeout}ms`,
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Handle network errors
|
|
190
|
+
if (error instanceof TypeError) {
|
|
191
|
+
throw new LedgerServiceError(`Network error: ${error.message}`);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
throw error;
|
|
195
|
+
}
|
|
196
|
+
}, this.config.retryPolicy);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Executes a function with exponential backoff retry logic
|
|
201
|
+
*/
|
|
202
|
+
private async executeWithRetry<T>(
|
|
203
|
+
fn: () => Promise<T>,
|
|
204
|
+
retryPolicy: RetryPolicy,
|
|
205
|
+
): Promise<T> {
|
|
206
|
+
let lastError: Error | undefined;
|
|
207
|
+
let delay = retryPolicy.initialDelayMs;
|
|
208
|
+
|
|
209
|
+
for (let attempt = 0; attempt <= retryPolicy.maxRetries; attempt++) {
|
|
210
|
+
try {
|
|
211
|
+
return await fn();
|
|
212
|
+
} catch (error) {
|
|
213
|
+
lastError = error as Error;
|
|
214
|
+
|
|
215
|
+
// Don't retry on certain errors
|
|
216
|
+
if (
|
|
217
|
+
isVersionConflictError(error) ||
|
|
218
|
+
isNotFoundError(error) ||
|
|
219
|
+
isAuthenticationError(error) ||
|
|
220
|
+
(isLedgerServiceError(error) &&
|
|
221
|
+
error.statusCode !== undefined &&
|
|
222
|
+
error.statusCode >= 400 &&
|
|
223
|
+
error.statusCode < 500)
|
|
224
|
+
) {
|
|
225
|
+
throw error;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Don't retry if this was the last attempt
|
|
229
|
+
if (attempt === retryPolicy.maxRetries) {
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Log retry attempt
|
|
234
|
+
this.logRetry(
|
|
235
|
+
attempt + 1,
|
|
236
|
+
retryPolicy.maxRetries,
|
|
237
|
+
delay,
|
|
238
|
+
error as Error,
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
// Wait before retrying
|
|
242
|
+
await this.sleep(delay);
|
|
243
|
+
|
|
244
|
+
// Calculate next delay with exponential backoff
|
|
245
|
+
delay = Math.min(
|
|
246
|
+
delay * retryPolicy.backoffMultiplier,
|
|
247
|
+
retryPolicy.maxDelayMs,
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
throw lastError;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Handles HTTP response and converts to typed result or throws appropriate error
|
|
257
|
+
*/
|
|
258
|
+
private async handleResponse<T>(response: Response): Promise<T> {
|
|
259
|
+
const contentType = response.headers.get("content-type");
|
|
260
|
+
const isJson = contentType?.includes("application/json");
|
|
261
|
+
|
|
262
|
+
// Handle success responses
|
|
263
|
+
if (response.ok) {
|
|
264
|
+
if (response.status === 204) {
|
|
265
|
+
// No content
|
|
266
|
+
return undefined as T;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const data = isJson ? await response.json() : await response.text();
|
|
270
|
+
this.logResponse(response.status, data);
|
|
271
|
+
return data as T;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Handle error responses
|
|
275
|
+
let errorData: unknown;
|
|
276
|
+
try {
|
|
277
|
+
errorData = isJson ? await response.json() : await response.text();
|
|
278
|
+
} catch {
|
|
279
|
+
errorData = null;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
this.logResponse(response.status, errorData);
|
|
283
|
+
const errorResponse = errorData && typeof errorData === "object"
|
|
284
|
+
? errorData as Partial<ErrorResponse>
|
|
285
|
+
: null;
|
|
286
|
+
|
|
287
|
+
// Handle specific error status codes
|
|
288
|
+
switch (response.status) {
|
|
289
|
+
case 401:
|
|
290
|
+
case 403:
|
|
291
|
+
throw new AuthenticationError(
|
|
292
|
+
`Authentication failed: ${response.statusText}`,
|
|
293
|
+
response.status,
|
|
294
|
+
errorResponse?.code ?? "unauthorized",
|
|
295
|
+
);
|
|
296
|
+
|
|
297
|
+
case 404:
|
|
298
|
+
throw new NotFoundError(
|
|
299
|
+
`Resource not found: ${response.statusText}`,
|
|
300
|
+
errorResponse?.code ?? "not_found",
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
case 409:
|
|
304
|
+
if (errorResponse?.code === "entity_conflict") {
|
|
305
|
+
throw new VersionConflictError(
|
|
306
|
+
"Version conflict detected",
|
|
307
|
+
errorData as ConflictResponse,
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
if (errorResponse?.code === "invalid_entity_version_chain") {
|
|
311
|
+
throw new EntityVersionChainError(
|
|
312
|
+
errorData as EntityVersionChainErrorResponse,
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
if (errorResponse?.code === "idempotency_mismatch") {
|
|
316
|
+
throw new IdempotencyMismatchError(errorData);
|
|
317
|
+
}
|
|
318
|
+
throw new LedgerServiceError(
|
|
319
|
+
`Conflict: ${response.statusText}`,
|
|
320
|
+
409,
|
|
321
|
+
errorData,
|
|
322
|
+
errorResponse?.code,
|
|
323
|
+
);
|
|
324
|
+
|
|
325
|
+
case 400:
|
|
326
|
+
throw new LedgerServiceError(
|
|
327
|
+
`Bad request: ${JSON.stringify(errorData)}`,
|
|
328
|
+
400,
|
|
329
|
+
errorData,
|
|
330
|
+
errorResponse?.code,
|
|
331
|
+
);
|
|
332
|
+
|
|
333
|
+
case 422:
|
|
334
|
+
// Ledger names the offending event and key, which is the reason
|
|
335
|
+
// shown for a rejected batch.
|
|
336
|
+
throw new LedgerServiceError(
|
|
337
|
+
errorResponse?.message ?? `Rejected: ${response.statusText}`,
|
|
338
|
+
422,
|
|
339
|
+
errorData,
|
|
340
|
+
errorResponse?.code,
|
|
341
|
+
);
|
|
342
|
+
|
|
343
|
+
case 500:
|
|
344
|
+
case 502:
|
|
345
|
+
case 503:
|
|
346
|
+
case 504:
|
|
347
|
+
throw new LedgerServiceError(
|
|
348
|
+
`Server error: ${response.statusText}`,
|
|
349
|
+
response.status,
|
|
350
|
+
errorData,
|
|
351
|
+
errorResponse?.code,
|
|
352
|
+
);
|
|
353
|
+
|
|
354
|
+
default:
|
|
355
|
+
throw new LedgerServiceError(
|
|
356
|
+
`HTTP ${response.status}: ${response.statusText}`,
|
|
357
|
+
response.status,
|
|
358
|
+
errorData,
|
|
359
|
+
errorResponse?.code,
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Logs an outgoing request
|
|
366
|
+
*/
|
|
367
|
+
private logRequest(method: string, url: string, _body?: unknown): void {
|
|
368
|
+
if (typeof console !== "undefined") {
|
|
369
|
+
console.debug(`[LedgerClient] ${method} ${url}`);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Logs a response
|
|
375
|
+
*/
|
|
376
|
+
private logResponse(status: number, _data: unknown): void {
|
|
377
|
+
if (typeof console !== "undefined") {
|
|
378
|
+
console.debug(`[LedgerClient] Response ${status}`);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Logs a retry attempt
|
|
384
|
+
*/
|
|
385
|
+
private logRetry(
|
|
386
|
+
attempt: number,
|
|
387
|
+
maxRetries: number,
|
|
388
|
+
delayMs: number,
|
|
389
|
+
error: Error,
|
|
390
|
+
): void {
|
|
391
|
+
if (typeof console !== "undefined") {
|
|
392
|
+
console.warn(
|
|
393
|
+
`[LedgerClient] Retry ${attempt}/${maxRetries} after ${delayMs}ms due to:`,
|
|
394
|
+
error.message,
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Sleep utility for retry delays
|
|
401
|
+
*/
|
|
402
|
+
private sleep(ms: number): Promise<void> {
|
|
403
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// ============================================================================
|
|
407
|
+
// Log Management Endpoints
|
|
408
|
+
// ============================================================================
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Lists all available logs
|
|
412
|
+
* GET /logs
|
|
413
|
+
*/
|
|
414
|
+
async listLogs(): Promise<LogSummary[]> {
|
|
415
|
+
const response = await this.listLogsResponse();
|
|
416
|
+
return response.logs;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
async listLogsResponse(): Promise<LogListResponse> {
|
|
420
|
+
return await this.request<LogListResponse>({
|
|
421
|
+
method: "GET",
|
|
422
|
+
path: "/logs",
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Creates a new log
|
|
428
|
+
* POST /logs
|
|
429
|
+
*/
|
|
430
|
+
async createLog(request: CreateLogRequest): Promise<CreateLogResponse> {
|
|
431
|
+
return await this.request<CreateLogResponse>({
|
|
432
|
+
method: "POST",
|
|
433
|
+
path: "/logs",
|
|
434
|
+
body: request,
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
async forkLog(sourceLogId: string, request: ForkLogRequest): Promise<LogSummary> {
|
|
439
|
+
return await this.request<LogSummary>({
|
|
440
|
+
method: 'POST',
|
|
441
|
+
path: `/logs/${encodeURIComponent(sourceLogId)}/forks`,
|
|
442
|
+
body: request,
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
async setLogVisibility(logId: string, hidden: boolean): Promise<LogSummary> {
|
|
447
|
+
return await this.request<LogSummary>({
|
|
448
|
+
method: 'POST',
|
|
449
|
+
path: `/logs/${encodeURIComponent(logId)}/visibility`,
|
|
450
|
+
body: { hidden },
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
async finalizeFork(logId: string): Promise<LogSummary> {
|
|
455
|
+
return await this.request<LogSummary>({
|
|
456
|
+
method: 'POST',
|
|
457
|
+
path: `/logs/${encodeURIComponent(logId)}/finalize-fork`,
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Gets metadata for a specific log
|
|
463
|
+
* GET /logs/{logId}
|
|
464
|
+
*/
|
|
465
|
+
async getLog(logId: string): Promise<LogMetadata> {
|
|
466
|
+
return await this.request<LogMetadata>({
|
|
467
|
+
method: "GET",
|
|
468
|
+
path: `/logs/${encodeURIComponent(logId)}`,
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Deletes a log
|
|
474
|
+
* DELETE /logs/{logId}
|
|
475
|
+
*/
|
|
476
|
+
async deleteLog(logId: string): Promise<void> {
|
|
477
|
+
await this.request<void>({
|
|
478
|
+
method: "DELETE",
|
|
479
|
+
path: `/logs/${encodeURIComponent(logId)}`,
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// ============================================================================
|
|
484
|
+
// Event Append Endpoint
|
|
485
|
+
// ============================================================================
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Appends events to a log
|
|
489
|
+
* POST /logs/{logId}/events
|
|
490
|
+
*
|
|
491
|
+
* @throws {VersionConflictError} when entity version conflict occurs (HTTP 409)
|
|
492
|
+
*/
|
|
493
|
+
async appendEvents(
|
|
494
|
+
logId: string,
|
|
495
|
+
request: AppendEventsRequest,
|
|
496
|
+
): Promise<AppendEventsResponse> {
|
|
497
|
+
await this.getWorkspaceRestoreSupport();
|
|
498
|
+
return await this.request<AppendEventsResponse>({
|
|
499
|
+
method: "POST",
|
|
500
|
+
path: `/logs/${encodeURIComponent(logId)}/events`,
|
|
501
|
+
body: request,
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// ============================================================================
|
|
506
|
+
// Event Query Endpoints
|
|
507
|
+
// ============================================================================
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Gets events from a log, optionally after a specific version
|
|
511
|
+
* GET /logs/{logId}/events
|
|
512
|
+
*/
|
|
513
|
+
async getEvents(
|
|
514
|
+
logId: string,
|
|
515
|
+
afterVersion?: number,
|
|
516
|
+
): Promise<StoredEvent[]> {
|
|
517
|
+
const events: StoredEvent[] = [];
|
|
518
|
+
let cursor = afterVersion ?? 0;
|
|
519
|
+
while (true) {
|
|
520
|
+
const page = await this.getEventPage(logId, cursor);
|
|
521
|
+
events.push(...page);
|
|
522
|
+
if (page.length < 512) return events;
|
|
523
|
+
cursor = page[page.length - 1].version;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
async getEventPage(
|
|
528
|
+
logId: string,
|
|
529
|
+
afterVersion = 0,
|
|
530
|
+
limit = 512,
|
|
531
|
+
): Promise<StoredEvent[]> {
|
|
532
|
+
await this.getWorkspaceRestoreSupport();
|
|
533
|
+
const response = await this.request<EventBatch>({
|
|
534
|
+
method: "GET",
|
|
535
|
+
path: `/logs/${encodeURIComponent(logId)}/events`,
|
|
536
|
+
queryParams: { afterVersion, limit },
|
|
537
|
+
});
|
|
538
|
+
return response.events;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// ============================================================================
|
|
542
|
+
// WebSocket realtime delivery
|
|
543
|
+
// ============================================================================
|
|
544
|
+
|
|
545
|
+
createEventSocket(logId: string, afterVersion = 0): EventSocket {
|
|
546
|
+
return new EventSocket({
|
|
547
|
+
afterVersion,
|
|
548
|
+
urlProvider: (cursor) =>
|
|
549
|
+
buildEventSocketUrl(
|
|
550
|
+
this.baseUrl,
|
|
551
|
+
logId,
|
|
552
|
+
cursor,
|
|
553
|
+
typeof window === "undefined"
|
|
554
|
+
? undefined
|
|
555
|
+
: window.location.href,
|
|
556
|
+
this.supportsWorkspaceRestore ? this.replayVersion() : 0,
|
|
557
|
+
),
|
|
558
|
+
autoReconnect: true,
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
export function buildEventSocketUrl(
|
|
564
|
+
baseUrl: string,
|
|
565
|
+
logId: string,
|
|
566
|
+
afterVersion: number,
|
|
567
|
+
browserUrl?: string,
|
|
568
|
+
replayVersion = 0,
|
|
569
|
+
): string {
|
|
570
|
+
const path =
|
|
571
|
+
`${baseUrl.replace(/\/$/, "")}/logs/${encodeURIComponent(logId)}` +
|
|
572
|
+
"/events/socket";
|
|
573
|
+
let socket: URL;
|
|
574
|
+
if (baseUrl.startsWith("/")) {
|
|
575
|
+
if (!browserUrl) {
|
|
576
|
+
throw new Error(
|
|
577
|
+
"A browser URL is required for a relative Ledger service URL",
|
|
578
|
+
);
|
|
579
|
+
}
|
|
580
|
+
socket = new URL(path, new URL(browserUrl).origin);
|
|
581
|
+
} else {
|
|
582
|
+
socket = new URL(path);
|
|
583
|
+
}
|
|
584
|
+
if (socket.protocol === "https:") socket.protocol = "wss:";
|
|
585
|
+
else if (socket.protocol === "http:") socket.protocol = "ws:";
|
|
586
|
+
else throw new Error("Ledger service URL must use HTTP or HTTPS");
|
|
587
|
+
socket.searchParams.set("afterVersion", String(afterVersion));
|
|
588
|
+
if (replayVersion > 0) socket.searchParams.set("replayVersion", String(replayVersion));
|
|
589
|
+
return socket.toString();
|
|
590
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ledger Service client library
|
|
3
|
+
*
|
|
4
|
+
* This module provides the main exports for interacting with the Ledger Service API.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// Configuration
|
|
8
|
+
export {
|
|
9
|
+
loadLedgerConfig,
|
|
10
|
+
type LedgerConfig,
|
|
11
|
+
type RetryPolicy,
|
|
12
|
+
ConfigValidationError,
|
|
13
|
+
} from "./config.js";
|
|
14
|
+
|
|
15
|
+
// Error classes and type guards
|
|
16
|
+
export {
|
|
17
|
+
LedgerServiceError,
|
|
18
|
+
VersionConflictError,
|
|
19
|
+
NotFoundError,
|
|
20
|
+
AuthenticationError,
|
|
21
|
+
EntityVersionChainError,
|
|
22
|
+
IdempotencyMismatchError,
|
|
23
|
+
isLedgerServiceError,
|
|
24
|
+
isVersionConflictError,
|
|
25
|
+
isNotFoundError,
|
|
26
|
+
isAuthenticationError,
|
|
27
|
+
} from "./errors.js";
|
|
28
|
+
|
|
29
|
+
// Ledger transport
|
|
30
|
+
export { LedgerClient } from "./http-client.js";
|
|
31
|
+
export { getLedgerClient } from './client.js';
|
|
32
|
+
export {
|
|
33
|
+
LedgerAdminClient,
|
|
34
|
+
LedgerAdminError,
|
|
35
|
+
LedgerAdminUnauthorizedError,
|
|
36
|
+
type AdminAppListResponse,
|
|
37
|
+
type AdminAppSummary,
|
|
38
|
+
type AdminAppTokenResponse,
|
|
39
|
+
type AdminErrorResponse,
|
|
40
|
+
type AdminSession,
|
|
41
|
+
} from "./admin-client.js";
|
|
42
|
+
|
|
43
|
+
// Types
|
|
44
|
+
export * from "./types.js";
|
|
45
|
+
|
|
46
|
+
// Entity ID utilities
|
|
47
|
+
export {
|
|
48
|
+
extractEntityId,
|
|
49
|
+
extractEntityIds,
|
|
50
|
+
groupEventsByEntity,
|
|
51
|
+
} from "./entity-id.js";
|
|
52
|
+
|
|
53
|
+
// WebSocket realtime delivery
|
|
54
|
+
export {
|
|
55
|
+
EventSocket,
|
|
56
|
+
createEventSocket,
|
|
57
|
+
SocketState,
|
|
58
|
+
SocketProtocolError,
|
|
59
|
+
LEDGER_SOCKET_PROTOCOL,
|
|
60
|
+
type EventSocketConfig,
|
|
61
|
+
type SocketFrame,
|
|
62
|
+
type SocketUrlProvider,
|
|
63
|
+
type SocketResyncReason,
|
|
64
|
+
} from "./socket.js";
|
|
65
|
+
|
|
66
|
+
// Version tracking
|
|
67
|
+
export { EntityVersionTracker } from "./version-tracker.js";
|