@delali/sirannon-db 0.2.3-next.26 → 0.2.3-next.28

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.
@@ -737,6 +737,12 @@ function refusalErrorCode(code) {
737
737
 
738
738
  // src/client/transport/ws-headers.ts
739
739
  var HEADERS_UNSUPPORTED_MESSAGE = "This runtime builds a WebSocket from the global constructor, which carries no handshake header, so 'headers' reaches the server on HTTP requests but never on the WebSocket upgrade. Carry the credential in 'webSocketProtocols' as well, which a browser handshake does carry, or create the client with { transport: 'http' }.";
740
+ var SUBPROTOCOL_TOKEN = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
741
+ var MALFORMED_SUBPROTOCOL_REASON = "a subprotocol is one or more of the characters a header token allows, so it carries no space, comma, or quotation mark and is never empty";
742
+ var REPEATED_SUBPROTOCOL_REASON = "a handshake refuses an offer that repeats a subprotocol";
743
+ function subprotocolMessage(index, reason) {
744
+ return `Entry ${index} of 'webSocketProtocols' cannot be offered, because ${reason}. The entry carries a credential, so it is left out of this message.`;
745
+ }
740
746
  function currentRuntime() {
741
747
  return globalThis;
742
748
  }
@@ -753,6 +759,24 @@ function assertHandshakeHeadersSupported(headers, webSocketProtocols, runtime =
753
759
  if (runtimeSupportsHandshakeHeaders(runtime)) return;
754
760
  throw new RemoteError("INVALID_ARGUMENT", HEADERS_UNSUPPORTED_MESSAGE);
755
761
  }
762
+ function assertWebSocketProtocolsValid(webSocketProtocols) {
763
+ if (webSocketProtocols === void 0) return;
764
+ const offered = typeof webSocketProtocols === "string" ? [webSocketProtocols] : webSocketProtocols;
765
+ const seen = /* @__PURE__ */ new Set();
766
+ for (const [index, value] of offered.entries()) {
767
+ if (!SUBPROTOCOL_TOKEN.test(value)) {
768
+ throw new RemoteError("INVALID_ARGUMENT", subprotocolMessage(index, MALFORMED_SUBPROTOCOL_REASON));
769
+ }
770
+ if (seen.has(value)) {
771
+ throw new RemoteError("INVALID_ARGUMENT", subprotocolMessage(index, REPEATED_SUBPROTOCOL_REASON));
772
+ }
773
+ seen.add(value);
774
+ }
775
+ }
776
+ function assertWebSocketCredentials(headers, webSocketProtocols, runtime = currentRuntime()) {
777
+ assertWebSocketProtocolsValid(webSocketProtocols);
778
+ assertHandshakeHeadersSupported(headers, webSocketProtocols, runtime);
779
+ }
756
780
 
757
781
  // src/client/transport/ws-connect.ts
758
782
  function createSocket(url, options) {
@@ -1382,7 +1406,7 @@ var WebSocketTransport = class {
1382
1406
  function resolveTransportSettings(options) {
1383
1407
  const transport = options?.transport ?? "websocket";
1384
1408
  if (transport === "websocket") {
1385
- assertHandshakeHeadersSupported(options?.headers, options?.webSocketProtocols);
1409
+ assertWebSocketCredentials(options?.headers, options?.webSocketProtocols);
1386
1410
  }
1387
1411
  return {
1388
1412
  transport,
@@ -1446,4 +1470,4 @@ var DatabaseClient = class {
1446
1470
  }
1447
1471
  };
1448
1472
 
1449
- export { DEFAULT_HTTP_REQUEST_TIMEOUT_MS, DatabaseClient, HttpTransport, RemoteDatabase, RemoteError, RemoteLiveQuery, RemoteSubscriptionBuilderImpl, SQL_REFUSED_MESSAGE, STAGED_STREAM_CAPABILITY, ServerCapabilities, WebSocketTransport, createEndpointTransport, decodeTaggedValues, encodeTaggedValues, postJson, toBaseUrl, toServerBaseUrl, toWsUrl, unrefTimer, verifyDeviceSyncCapabilities };
1473
+ export { DEFAULT_HTTP_REQUEST_TIMEOUT_MS, DatabaseClient, HttpTransport, RemoteDatabase, RemoteError, RemoteLiveQuery, RemoteSubscriptionBuilderImpl, SQL_REFUSED_MESSAGE, STAGED_STREAM_CAPABILITY, ServerCapabilities, WebSocketTransport, assertWebSocketCredentials, createEndpointTransport, decodeTaggedValues, encodeTaggedValues, postJson, toBaseUrl, toServerBaseUrl, toWsUrl, unrefTimer, verifyDeviceSyncCapabilities };
@@ -161,8 +161,10 @@ interface SyncControllerOptions {
161
161
  databaseId: string;
162
162
  /** Tables this device syncs. */
163
163
  tables: readonly string[];
164
- /** Headers attached to every request and to the WebSocket upgrade. */
164
+ /** Headers attached to every HTTP request, and to the pull subscription's WebSocket upgrade in a runtime whose WebSocket carries a handshake header. */
165
165
  headers?: Record<string, string>;
166
+ /** Subprotocols offered on the pull subscription's WebSocket upgrade, which is how a browser device carries a credential. The controller offers `sirannon.v1` ahead of them. */
167
+ webSocketProtocols?: string | string[];
166
168
  /** Changes sent in one push. */
167
169
  batchSize?: number;
168
170
  /** Milliseconds between pushes of locally recorded changes. */
@@ -187,6 +189,8 @@ interface SyncControllerOptions {
187
189
  resolver?: ConflictResolver | ((table: string) => ConflictResolver);
188
190
  /** Called for each change this device pulls. */
189
191
  onChange?: (event: ChangeEvent) => void;
192
+ /** Called with this device's status when the controller changes state, pushes a batch, applies a pulled batch, needs a resync, or records or clears an error. */
193
+ onStatusChange?: (status: SyncStatus) => void;
190
194
  /** Called when the server says the device must download a fresh snapshot. */
191
195
  onResyncRequired?: () => void;
192
196
  /** Called as each snapshot page arrives. */
@@ -275,15 +279,20 @@ declare class SyncController {
275
279
  private readonly pull;
276
280
  private readonly push;
277
281
  private readonly resync;
282
+ private readonly statusChanges;
278
283
  private port;
279
284
  private deviceId;
280
285
  private capabilities;
281
286
  private schemaVersion;
282
- private state;
287
+ private syncState;
283
288
  private pullRetryTimer;
284
289
  private consecutivePullFailures;
290
+ private pendingPushCount;
285
291
  private lastError;
286
292
  constructor(db: Database, options: SyncControllerOptions);
293
+ private get state();
294
+ private setState;
295
+ private setError;
287
296
  /**
288
297
  * Connects to the server and starts pushing and pulling changes.
289
298
  */
@@ -306,6 +315,8 @@ declare class SyncController {
306
315
  * @returns The device's state, cursors, pending push count, and last failure.
307
316
  */
308
317
  status(): Promise<SyncStatus>;
318
+ private captureStatus;
319
+ private refreshOutboxCount;
309
320
  /**
310
321
  * Pushes local changes now instead of waiting for the next interval.
311
322
  */
@@ -1,5 +1,5 @@
1
- import { DatabaseClient, createEndpointTransport, toBaseUrl, DEFAULT_HTTP_REQUEST_TIMEOUT_MS, postJson, decodeTaggedValues, RemoteError, encodeTaggedValues, STAGED_STREAM_CAPABILITY, verifyDeviceSyncCapabilities, unrefTimer, toWsUrl, WebSocketTransport } from '../chunk-WJ67DTD6.mjs';
2
- export { HttpTransport, RemoteDatabase, RemoteError, RemoteLiveQuery, RemoteSubscriptionBuilderImpl, SQL_REFUSED_MESSAGE, ServerCapabilities, WebSocketTransport } from '../chunk-WJ67DTD6.mjs';
1
+ import { DatabaseClient, createEndpointTransport, toBaseUrl, DEFAULT_HTTP_REQUEST_TIMEOUT_MS, postJson, decodeTaggedValues, RemoteError, encodeTaggedValues, assertWebSocketCredentials, STAGED_STREAM_CAPABILITY, verifyDeviceSyncCapabilities, unrefTimer, toWsUrl, WebSocketTransport } from '../chunk-3UHAPQBW.mjs';
2
+ export { HttpTransport, RemoteDatabase, RemoteError, RemoteLiveQuery, RemoteSubscriptionBuilderImpl, SQL_REFUSED_MESSAGE, ServerCapabilities, WebSocketTransport } from '../chunk-3UHAPQBW.mjs';
3
3
 
4
4
  // src/core/sync/canonicalise.ts
5
5
  function canonicaliseForChecksum(value) {
@@ -697,6 +697,7 @@ var PullStream = class {
697
697
  const encodedId = encodeURIComponent(this.config.databaseId);
698
698
  const transport = new WebSocketTransport(`${this.config.wsBaseUrl}/db/${encodedId}`, {
699
699
  headers: this.config.headers,
700
+ protocols: this.config.webSocketProtocols,
700
701
  requestTimeout: this.config.requestTimeout
701
702
  });
702
703
  this.transport = transport;
@@ -1053,6 +1054,10 @@ var ResyncScheduler = class {
1053
1054
  };
1054
1055
 
1055
1056
  // src/client/sync-controller-wiring.ts
1057
+ function describeError(err) {
1058
+ const code = err instanceof Error && "code" in err ? String(err.code) : "UNKNOWN_ERROR";
1059
+ return { code, message: err instanceof Error ? err.message : String(err) };
1060
+ }
1056
1061
  var DEFAULT_BATCH_SIZE = 100;
1057
1062
  var DEFAULT_PUSH_INTERVAL_MS = 1e3;
1058
1063
  var DEFAULT_ACK_INTERVAL_MS = 2e3;
@@ -1086,6 +1091,7 @@ function createSyncCollaborators(baseUrl, options, host) {
1086
1091
  databaseId: options.databaseId,
1087
1092
  tables: options.tables,
1088
1093
  headers: options.headers,
1094
+ webSocketProtocols: options.webSocketProtocols,
1089
1095
  ackIntervalMs: options.ackIntervalMs ?? DEFAULT_ACK_INTERVAL_MS,
1090
1096
  requestTimeout: options.requestTimeout,
1091
1097
  immediateAckAfterChanges: options.immediateAckAfterChanges,
@@ -1120,6 +1126,75 @@ function createSyncCollaborators(baseUrl, options, host) {
1120
1126
  return { push, pull, resync };
1121
1127
  }
1122
1128
 
1129
+ // src/client/sync-status-notifier.ts
1130
+ var MIN_OUTBOX_COUNT_INTERVAL_MS = 100;
1131
+ var SyncStatusNotifier = class {
1132
+ constructor(listener, capture, refreshOutboxCount) {
1133
+ this.listener = listener;
1134
+ this.capture = capture;
1135
+ this.refreshOutboxCount = refreshOutboxCount;
1136
+ }
1137
+ queued = [];
1138
+ flushing = false;
1139
+ counting = false;
1140
+ countTimer = null;
1141
+ lastCountAt = 0;
1142
+ /**
1143
+ * Captures the controller's status now and delivers it on a microtask.
1144
+ */
1145
+ notify() {
1146
+ if (this.listener === void 0) return;
1147
+ this.queued.push(this.capture());
1148
+ this.scheduleFlush();
1149
+ this.scheduleCount();
1150
+ }
1151
+ scheduleFlush() {
1152
+ if (this.flushing) return;
1153
+ this.flushing = true;
1154
+ queueMicrotask(() => {
1155
+ this.flushing = false;
1156
+ this.flush();
1157
+ });
1158
+ }
1159
+ flush() {
1160
+ const listener = this.listener;
1161
+ if (listener === void 0) return;
1162
+ while (this.queued.length > 0) {
1163
+ const status = this.queued.shift();
1164
+ if (status === void 0) return;
1165
+ try {
1166
+ listener(status);
1167
+ } catch {
1168
+ }
1169
+ }
1170
+ }
1171
+ scheduleCount() {
1172
+ if (this.counting || this.countTimer !== null) return;
1173
+ const waitMs = Math.max(0, this.lastCountAt + MIN_OUTBOX_COUNT_INTERVAL_MS - Date.now());
1174
+ this.countTimer = setTimeout(() => {
1175
+ this.countTimer = null;
1176
+ void this.runCount();
1177
+ }, waitMs);
1178
+ unrefTimer(this.countTimer);
1179
+ }
1180
+ async runCount() {
1181
+ this.counting = true;
1182
+ let changed = false;
1183
+ try {
1184
+ changed = await this.refreshOutboxCount();
1185
+ } catch {
1186
+ changed = false;
1187
+ } finally {
1188
+ this.lastCountAt = Date.now();
1189
+ this.counting = false;
1190
+ }
1191
+ if (changed) {
1192
+ this.queued.push(this.capture());
1193
+ this.scheduleFlush();
1194
+ }
1195
+ }
1196
+ };
1197
+
1123
1198
  // src/client/sync-controller.ts
1124
1199
  var SyncController = class {
1125
1200
  constructor(db, options) {
@@ -1128,19 +1203,24 @@ var SyncController = class {
1128
1203
  this.baseUrl = toBaseUrl(options.url);
1129
1204
  this.pushIntervalMs = options.pushIntervalMs ?? DEFAULT_PUSH_INTERVAL_MS;
1130
1205
  this.maxPushRetryDelayMs = options.maxPushRetryDelayMs ?? DEFAULT_MAX_PUSH_RETRY_DELAY_MS;
1206
+ assertWebSocketCredentials(options.headers, options.webSocketProtocols);
1207
+ this.statusChanges = new SyncStatusNotifier(
1208
+ options.onStatusChange,
1209
+ () => this.captureStatus(),
1210
+ () => this.refreshOutboxCount()
1211
+ );
1131
1212
  const collaborators = createSyncCollaborators(this.baseUrl, options, {
1132
1213
  state: () => this.state,
1133
1214
  port: () => this.port,
1134
1215
  schemaVersion: () => this.schemaVersion ?? 0,
1135
1216
  reconcileSchema: () => this.reconcileSchema(),
1136
1217
  recordError: (err) => this.recordError(err),
1137
- clearError: () => {
1138
- this.lastError = null;
1139
- },
1218
+ clearError: () => this.setError(null),
1140
1219
  markResyncRequired: () => this.markResyncRequired(),
1141
1220
  onApplyFailure: (err) => this.handleApplyFailure(err),
1142
1221
  onApplySuccess: () => {
1143
1222
  this.consecutivePullFailures = 0;
1223
+ this.statusChanges.notify();
1144
1224
  },
1145
1225
  download: () => this.downloadSnapshot({ pageSize: options.snapshotPageSize, onProgress: options.onSnapshotProgress })
1146
1226
  });
@@ -1154,20 +1234,34 @@ var SyncController = class {
1154
1234
  pull;
1155
1235
  push;
1156
1236
  resync;
1237
+ statusChanges;
1157
1238
  port = null;
1158
1239
  deviceId = null;
1159
1240
  capabilities = null;
1160
1241
  schemaVersion = null;
1161
- state = "stopped";
1242
+ syncState = "stopped";
1162
1243
  pullRetryTimer = null;
1163
1244
  consecutivePullFailures = 0;
1245
+ pendingPushCount = 0;
1164
1246
  lastError = null;
1247
+ get state() {
1248
+ return this.syncState;
1249
+ }
1250
+ setState(next) {
1251
+ if (this.syncState === next) return;
1252
+ this.syncState = next;
1253
+ this.statusChanges.notify();
1254
+ }
1255
+ setError(failure) {
1256
+ this.lastError = failure;
1257
+ this.statusChanges.notify();
1258
+ }
1165
1259
  /**
1166
1260
  * Connects to the server and starts pushing and pulling changes.
1167
1261
  */
1168
1262
  async start() {
1169
1263
  if (this.state === "running" || this.state === "starting") return;
1170
- this.state = "starting";
1264
+ this.setState("starting");
1171
1265
  try {
1172
1266
  await this.verifyCapabilities();
1173
1267
  this.pull.stagedStream = this.capabilities?.includes(STAGED_STREAM_CAPABILITY) ?? false;
@@ -1192,10 +1286,10 @@ var SyncController = class {
1192
1286
  if (!this.resync.required) {
1193
1287
  await this.openPull();
1194
1288
  }
1195
- this.state = "running";
1289
+ this.setState("running");
1196
1290
  } catch (err) {
1197
1291
  this.teardownStream();
1198
- this.state = "stopped";
1292
+ this.setState("stopped");
1199
1293
  throw err;
1200
1294
  }
1201
1295
  this.push.start();
@@ -1210,7 +1304,7 @@ var SyncController = class {
1210
1304
  pause() {
1211
1305
  if (this.state !== "running") return;
1212
1306
  this.teardownStream();
1213
- this.state = "paused";
1307
+ this.setState("paused");
1214
1308
  void this.pull.persist();
1215
1309
  }
1216
1310
  /**
@@ -1218,7 +1312,7 @@ var SyncController = class {
1218
1312
  */
1219
1313
  async resume() {
1220
1314
  if (this.state !== "paused") return;
1221
- this.state = "stopped";
1315
+ this.setState("stopped");
1222
1316
  await this.start();
1223
1317
  }
1224
1318
  /**
@@ -1227,7 +1321,7 @@ var SyncController = class {
1227
1321
  async stop() {
1228
1322
  if (this.state === "stopped") return;
1229
1323
  this.teardownStream();
1230
- this.state = "stopped";
1324
+ this.setState("stopped");
1231
1325
  await this.pull.persist();
1232
1326
  }
1233
1327
  /**
@@ -1236,20 +1330,29 @@ var SyncController = class {
1236
1330
  * @returns The device's state, cursors, pending push count, and last failure.
1237
1331
  */
1238
1332
  async status() {
1239
- const pendingPushCount = this.port ? await this.port.countOutboxPending(this.push.cursor) : 0;
1333
+ await this.refreshOutboxCount();
1334
+ return this.captureStatus();
1335
+ }
1336
+ captureStatus() {
1240
1337
  return {
1241
1338
  state: this.state,
1242
1339
  deviceId: this.deviceId,
1243
1340
  serverCapabilities: this.capabilities,
1244
1341
  schemaVersion: this.schemaVersion,
1245
- pendingPushCount,
1342
+ pendingPushCount: this.pendingPushCount,
1246
1343
  lastPushedSeq: this.push.cursor,
1247
1344
  lastPulledSeq: this.pull.pullSeq,
1248
- pushCaughtUp: pendingPushCount === 0,
1345
+ pushCaughtUp: this.pendingPushCount === 0,
1249
1346
  resyncRequired: this.resync.required,
1250
1347
  lastError: this.lastError
1251
1348
  };
1252
1349
  }
1350
+ async refreshOutboxCount() {
1351
+ const counted = this.port === null ? 0 : await this.port.countOutboxPending(this.push.cursor);
1352
+ const changed = counted !== this.pendingPushCount;
1353
+ this.pendingPushCount = counted;
1354
+ return changed;
1355
+ }
1253
1356
  /**
1254
1357
  * Pushes local changes now instead of waiting for the next interval.
1255
1358
  */
@@ -1283,16 +1386,17 @@ var SyncController = class {
1283
1386
  if (result.status === "resync-required") {
1284
1387
  this.markResyncRequired();
1285
1388
  } else if (result.status === "ahead") {
1286
- this.lastError = {
1389
+ this.setError({
1287
1390
  code: "SCHEMA_AHEAD",
1288
1391
  message: `Device schema version ${result.schemaVersion} is ahead of server version ${result.serverVersion}`
1289
- };
1392
+ });
1290
1393
  }
1291
1394
  return result.status;
1292
1395
  }
1293
1396
  markResyncRequired() {
1294
1397
  this.resync.markRequired();
1295
1398
  this.resync.schedule();
1399
+ this.statusChanges.notify();
1296
1400
  }
1297
1401
  /**
1298
1402
  * Replaces the local database with a fresh copy from the server and resumes syncing from it.
@@ -1311,7 +1415,7 @@ var SyncController = class {
1311
1415
  throw new Error("Snapshot download requires a started sync controller");
1312
1416
  }
1313
1417
  this.teardownStream();
1314
- this.state = "snapshotting";
1418
+ this.setState("snapshotting");
1315
1419
  try {
1316
1420
  await this.push.drainFully(port);
1317
1421
  await downloadDatabaseSnapshot(port, {
@@ -1325,22 +1429,22 @@ var SyncController = class {
1325
1429
  this.schemaVersion = await this.localSchemaVersion();
1326
1430
  await port.setResyncRequired(false);
1327
1431
  this.resync.recordSuccess();
1328
- this.lastError = null;
1432
+ this.setError(null);
1329
1433
  } catch (err) {
1330
1434
  const failure = describeError(err);
1331
- this.lastError = failure;
1435
+ this.setError(failure);
1332
1436
  this.resync.recordFailure();
1333
1437
  const databaseUsable = await this.snapshotGateOpen(port);
1334
- this.state = "stopped";
1438
+ this.setState("stopped");
1335
1439
  try {
1336
1440
  await this.start();
1337
1441
  } catch {
1338
- this.state = "paused";
1442
+ this.setState("paused");
1339
1443
  }
1340
1444
  this.resync.complete({ ok: false, error: failure, databaseUsable, retrying: this.resync.retryScheduled });
1341
1445
  throw err;
1342
1446
  }
1343
- this.state = "stopped";
1447
+ this.setState("stopped");
1344
1448
  try {
1345
1449
  await this.start();
1346
1450
  } finally {
@@ -1361,7 +1465,7 @@ var SyncController = class {
1361
1465
  }
1362
1466
  }
1363
1467
  recordError(err) {
1364
- this.lastError = describeError(err);
1468
+ this.setError(describeError(err));
1365
1469
  }
1366
1470
  handleApplyFailure(err) {
1367
1471
  this.recordError(err);
@@ -1407,7 +1511,7 @@ var SyncController = class {
1407
1511
  if (this.resync.required) return;
1408
1512
  if (status === "ahead") throw refusal;
1409
1513
  this.pull.teardown();
1410
- this.lastError = null;
1514
+ this.setError(null);
1411
1515
  await this.pull.open(deviceId, this.schemaVersion ?? 0);
1412
1516
  }
1413
1517
  teardownStream() {
@@ -1420,9 +1524,5 @@ var SyncController = class {
1420
1524
  this.pull.teardown();
1421
1525
  }
1422
1526
  };
1423
- function describeError(err) {
1424
- const code = err instanceof Error && "code" in err ? String(err.code) : "UNKNOWN_ERROR";
1425
- return { code, message: err instanceof Error ? err.message : String(err) };
1426
- }
1427
1527
 
1428
1528
  export { FieldMergeResolver, LWWResolver, PrimaryWinsResolver, SirannonClient, SyncController, downloadDatabaseSnapshot, encodeSyncBatch, pushSyncBatch };
@@ -1,4 +1,4 @@
1
- import { DatabaseClient, toBaseUrl, createEndpointTransport, RemoteError, toServerBaseUrl, unrefTimer } from '../chunk-WJ67DTD6.mjs';
1
+ import { DatabaseClient, toBaseUrl, createEndpointTransport, RemoteError, toServerBaseUrl, unrefTimer } from '../chunk-3UHAPQBW.mjs';
2
2
 
3
3
  // src/client/cluster-routing.ts
4
4
  function clusterRoutingFingerprint(state) {
@@ -1,17 +1,17 @@
1
1
  export { F as FieldMergeResolver, L as LWWResolver, P as PrimaryWinsResolver } from '../primary-wins-B0np8JS3.js';
2
2
  import { H as HLCTimestamp, R as ReplicationBatch, C as ConflictResolver, A as ApplyResult, S as SyncTableManifest } from '../types-CjhxcjhA.js';
3
3
  export { a as ConflictContext, b as ConflictResolution, c as ReplicationChange } from '../types-CjhxcjhA.js';
4
+ import { R as ReplicationStatusInfo } from '../server-options-BVAFmguz.js';
5
+ import { r as ClusterReadEndpointInfo, C as ClusterStatusInfo, e as SQLiteConnection, T as Transaction } from '../types-CXoPBeDM.js';
6
+ import { C as CoordinatorRuntimeStatus, g as ReplicationStatus, h as SyncPhase, I as InFlightBatch, P as PeerState, a as ForwardedTransactionResult, b as SyncBatch, c as SyncComplete, d as SyncAck, S as SyncRequest, i as ReplicationConfig, j as SyncState, k as ReplicationErrorEvent, R as ReplicationAck, l as Topology, f as TopologyRole } from '../types-DxoEm08T.js';
7
+ export { F as ForwardedTransaction, N as NodeInfo, e as ReplicationTransport, T as TransportConfig } from '../types-DxoEm08T.js';
4
8
  import { c as ReplicationGroupState, e as CoordinatorWatchDisposer } from '../types-CMBcFPhb.js';
5
9
  export { A as AcquireControllerLeaseInput, a as AcquireControllerLeaseResult, h as AdmitNodeToInSyncSetInput, C as ClusterCoordinator, f as CompareAndAdvancePrimaryTermInput, g as CompareAndAdvancePrimaryTermResult, j as CoordinatorCompatibilityMetadata, k as CoordinatorLease, b as CoordinatorNodeSession, l as CoordinatorPrimary, P as PromoteEligibleReplicaInput, R as RegisterNodeSessionInput, d as ReplicationGroupWatcher, S as SetReplicationGroupStateInput, U as UpdateInSyncSetInput, i as UpdateNodeMaintenanceInput } from '../types-CMBcFPhb.js';
6
10
  import { EventEmitter } from 'node:events';
7
11
  import { C as ChangeTracker } from '../change-tracker-C2Z8UbI0.js';
8
12
  import { D as Database } from '../database-DvSvONb-.js';
9
- import { e as SQLiteConnection, T as Transaction } from '../types-CXoPBeDM.js';
10
13
  import { P as Params, Q as QueryOptions, E as ExecuteResult } from '../query-types-BvkzxKQv.js';
11
- import { g as SyncPhase, I as InFlightBatch, P as PeerState, a as ForwardedTransactionResult, b as SyncBatch, c as SyncComplete, d as SyncAck, S as SyncRequest, h as ReplicationConfig, i as SyncState, j as ReplicationStatus, k as ReplicationErrorEvent, R as ReplicationAck, l as Topology, f as TopologyRole } from '../types-DtUwSmAQ.js';
12
- export { F as ForwardedTransaction, N as NodeInfo, e as ReplicationTransport, T as TransportConfig } from '../types-DtUwSmAQ.js';
13
14
  import { S as SirannonError } from '../errors-Dei4GdBb.js';
14
- import '../server-options-BVAFmguz.js';
15
15
  import '../operation-registry-6qErmUT2.js';
16
16
  import '../types-BCejqzNA.js';
17
17
 
@@ -64,6 +64,53 @@ declare class HLC {
64
64
  static encode(wallMs: number, logical: number, nodeId: string): string;
65
65
  }
66
66
 
67
+ /**
68
+ * What a node needs to know beyond its own engine state to describe its group.
69
+ *
70
+ * @public
71
+ */
72
+ interface ClusterStatusOptions {
73
+ /** Identifier of the database the reported status describes. */
74
+ databaseId: string;
75
+ /** Address a client reaches each node on, keyed by node id. */
76
+ endpoints: Readonly<Record<string, string>>;
77
+ }
78
+ /**
79
+ * Turns one node's engine status into the figures its readiness endpoint reports.
80
+ *
81
+ * @param status - Status the replication engine reports for this node.
82
+ * @returns The replication figures, ready to return from `getReplicationStatus`.
83
+ *
84
+ * @public
85
+ */
86
+ declare function toReplicationStatusInfo(status: ReplicationStatus): ReplicationStatusInfo;
87
+ /**
88
+ * Turns one node's engine status into what `GET /db/{id}/cluster` reports about its group.
89
+ *
90
+ * @param status - Status the replication engine reports for this node.
91
+ * @param options - The database this status describes and the address of each node.
92
+ * @returns The group status, ready to return from `getClusterStatus`.
93
+ *
94
+ * @public
95
+ */
96
+ declare function toClusterStatusInfo(status: ReplicationStatus, options: ClusterStatusOptions): ClusterStatusInfo;
97
+ /**
98
+ * Lists every node a client can read from, with the read concerns each one serves.
99
+ *
100
+ * A node counts towards majority and is neither quarantined, being taken out of
101
+ * service, nor being rebuilt to appear at all. A node the group counts as in
102
+ * sync serves both `local` and `majority`; one that has fallen behind serves
103
+ * `local` alone, because the engine answers a `local` read without any in-sync
104
+ * check.
105
+ *
106
+ * @param coordinator - Group state this node last read from the coordinator.
107
+ * @param endpoints - Address a client reaches each node on, keyed by node id.
108
+ * @returns One entry per node a client can read from.
109
+ *
110
+ * @public
111
+ */
112
+ declare function toClusterReadEndpoints(coordinator: CoordinatorRuntimeStatus, endpoints: Readonly<Record<string, string>>): ClusterReadEndpointInfo[];
113
+
67
114
  /**
68
115
  * Persistent change log that bridges CDC events and the replication protocol.
69
116
  *
@@ -668,4 +715,4 @@ declare class PrimaryReplicaTopology implements Topology {
668
715
  requiresConflictResolution(): boolean;
669
716
  }
670
717
 
671
- export { ApplyResult, AuthorityError, BatchValidationError, ConflictError, ConflictResolver, CoordinatorError, CoordinatorWatchDisposer, FailoverError, ForwardedTransactionResult, HLC, HLCTimestamp, NoSafePrimaryError, NodeDrainingError, NodeNotInSyncError, PeerState, PeerTracker, PrimaryReplicaTopology, ProtocolVersionMismatchError, ReadConcernError, ReplicationAck, ReplicationBatch, ReplicationConfig, ReplicationEngine, ReplicationError, ReplicationErrorEvent, ReplicationGroupState, ReplicationLog, ReplicationStatus, StalePrimaryError, SyncAck, SyncBatch, SyncComplete, SyncError, SyncPhase, SyncRequest, SyncState, SyncTableManifest, Topology, TopologyError, TopologyRole, TransportError, UnsafeRecoveryRequiredError, WriteConcernError, generateNodeId, validateNodeId };
718
+ export { ApplyResult, AuthorityError, BatchValidationError, type ClusterStatusOptions, ConflictError, ConflictResolver, CoordinatorError, CoordinatorWatchDisposer, FailoverError, ForwardedTransactionResult, HLC, HLCTimestamp, NoSafePrimaryError, NodeDrainingError, NodeNotInSyncError, PeerState, PeerTracker, PrimaryReplicaTopology, ProtocolVersionMismatchError, ReadConcernError, ReplicationAck, ReplicationBatch, ReplicationConfig, ReplicationEngine, ReplicationError, ReplicationErrorEvent, ReplicationGroupState, ReplicationLog, ReplicationStatus, StalePrimaryError, SyncAck, SyncBatch, SyncComplete, SyncError, SyncPhase, SyncRequest, SyncState, SyncTableManifest, Topology, TopologyError, TopologyRole, TransportError, UnsafeRecoveryRequiredError, WriteConcernError, generateNodeId, toClusterReadEndpoints, toClusterStatusInfo, toReplicationStatusInfo, validateNodeId };
@@ -118,6 +118,50 @@ var PrimaryWinsResolver = class {
118
118
  }
119
119
  };
120
120
 
121
+ // src/replication/cluster-status.ts
122
+ function toReplicationStatusInfo(status) {
123
+ const coordinator = status.coordinator;
124
+ return {
125
+ role: status.role,
126
+ writeForwarding: true,
127
+ peers: status.peers.length,
128
+ localSeq: status.localSeq,
129
+ health: status.health,
130
+ replicationGroupId: coordinator?.groupId,
131
+ primaryTerm: coordinator?.primaryTerm,
132
+ currentPrimary: coordinator?.currentPrimary?.nodeId,
133
+ coordinator: coordinator && { connected: coordinator.connected, authority: coordinator.authority },
134
+ controller: coordinator && { state: coordinator.controllerState },
135
+ inSyncReplicas: coordinator?.inSyncNodeIds.filter((nodeId) => nodeId !== coordinator.currentPrimary?.nodeId),
136
+ laggingReplicas: coordinator?.votingDataBearingNodeIds.filter(
137
+ (nodeId) => !coordinator.inSyncNodeIds.includes(nodeId)
138
+ ),
139
+ syncState: status.syncState?.phase
140
+ };
141
+ }
142
+ function toClusterStatusInfo(status, options) {
143
+ const coordinator = status.coordinator;
144
+ return {
145
+ databaseId: options.databaseId,
146
+ replicationGroupId: coordinator?.groupId,
147
+ role: status.role,
148
+ currentPrimary: coordinator?.currentPrimary ? { ...coordinator.currentPrimary } : coordinator?.currentPrimary ?? null,
149
+ primaryTerm: coordinator?.primaryTerm,
150
+ readEndpoints: coordinator && toClusterReadEndpoints(coordinator, options.endpoints),
151
+ health: status.health.state,
152
+ healthReason: status.health.reason
153
+ };
154
+ }
155
+ function toClusterReadEndpoints(coordinator, endpoints) {
156
+ return coordinator.votingDataBearingNodeIds.filter(
157
+ (nodeId) => !coordinator.faultedNodeIds.includes(nodeId) && !coordinator.drainingNodeIds.includes(nodeId) && !coordinator.repairingNodeIds.includes(nodeId)
158
+ ).map((nodeId) => ({
159
+ nodeId,
160
+ endpoint: endpoints[nodeId] ?? "",
161
+ readConcerns: coordinator.inSyncNodeIds.includes(nodeId) ? ["local", "majority"] : ["local"]
162
+ }));
163
+ }
164
+
121
165
  // src/replication/log/schema.ts
122
166
  var SchemaOps = class {
123
167
  constructor(conn, changesTable) {
@@ -3256,4 +3300,4 @@ var PrimaryReplicaTopology = class {
3256
3300
  }
3257
3301
  };
3258
3302
 
3259
- export { FieldMergeResolver, PeerTracker, PrimaryReplicaTopology, PrimaryWinsResolver, ReplicationEngine, ReplicationLog, generateNodeId, validateNodeId };
3303
+ export { FieldMergeResolver, PeerTracker, PrimaryReplicaTopology, PrimaryWinsResolver, ReplicationEngine, ReplicationLog, generateNodeId, toClusterReadEndpoints, toClusterStatusInfo, toReplicationStatusInfo, validateNodeId };
@@ -1,7 +1,7 @@
1
1
  import { BinaryWriter, BinaryReader } from '@bufbuild/protobuf/wire';
2
2
  import { Client, ClientDuplexStream, CallOptions, Metadata, ServiceError, ClientUnaryCall, ChannelCredentials, ClientOptions, ServerDuplexStream, Server } from '@grpc/grpc-js';
3
3
  import { HealthImplementation } from 'grpc-health-check';
4
- import { R as ReplicationAck, F as ForwardedTransaction, a as ForwardedTransactionResult, N as NodeInfo, S as SyncRequest, b as SyncBatch, c as SyncComplete, d as SyncAck, e as ReplicationTransport, f as TopologyRole, T as TransportConfig } from '../types-DtUwSmAQ.js';
4
+ import { R as ReplicationAck, F as ForwardedTransaction, a as ForwardedTransactionResult, N as NodeInfo, S as SyncRequest, b as SyncBatch, c as SyncComplete, d as SyncAck, e as ReplicationTransport, f as TopologyRole, T as TransportConfig } from '../types-DxoEm08T.js';
5
5
  import { R as ReplicationBatch } from '../types-CjhxcjhA.js';
6
6
  import '../change-tracker-C2Z8UbI0.js';
7
7
  import '../types-CXoPBeDM.js';
@@ -1,4 +1,4 @@
1
- import { R as ReplicationAck, F as ForwardedTransaction, a as ForwardedTransactionResult, S as SyncRequest, b as SyncBatch, c as SyncComplete, d as SyncAck, N as NodeInfo, e as ReplicationTransport, T as TransportConfig } from '../types-DtUwSmAQ.js';
1
+ import { R as ReplicationAck, F as ForwardedTransaction, a as ForwardedTransactionResult, S as SyncRequest, b as SyncBatch, c as SyncComplete, d as SyncAck, N as NodeInfo, e as ReplicationTransport, T as TransportConfig } from '../types-DxoEm08T.js';
2
2
  import { R as ReplicationBatch } from '../types-CjhxcjhA.js';
3
3
  import '../change-tracker-C2Z8UbI0.js';
4
4
  import '../types-CXoPBeDM.js';
@@ -496,4 +496,4 @@ interface ReplicationErrorEvent {
496
496
  recoverable: boolean;
497
497
  }
498
498
 
499
- export type { ForwardedTransaction as F, InFlightBatch as I, NodeInfo as N, PeerState as P, ReplicationAck as R, SyncRequest as S, TransportConfig as T, ForwardedTransactionResult as a, SyncBatch as b, SyncComplete as c, SyncAck as d, ReplicationTransport as e, TopologyRole as f, SyncPhase as g, ReplicationConfig as h, SyncState as i, ReplicationStatus as j, ReplicationErrorEvent as k, Topology as l };
499
+ export type { CoordinatorRuntimeStatus as C, ForwardedTransaction as F, InFlightBatch as I, NodeInfo as N, PeerState as P, ReplicationAck as R, SyncRequest as S, TransportConfig as T, ForwardedTransactionResult as a, SyncBatch as b, SyncComplete as c, SyncAck as d, ReplicationTransport as e, TopologyRole as f, ReplicationStatus as g, SyncPhase as h, ReplicationConfig as i, SyncState as j, ReplicationErrorEvent as k, Topology as l };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@delali/sirannon-db",
3
3
  "type": "module",
4
- "version": "0.2.3-next.26",
4
+ "version": "0.2.3-next.28",
5
5
  "description": "A production-grade library that turns SQLite databases into a networked data layer with real-time subscriptions.",
6
6
  "author": "Delali (https://sondelali.com)",
7
7
  "license": "Apache-2.0",