@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.
- package/dist/{chunk-WJ67DTD6.mjs → chunk-3UHAPQBW.mjs} +26 -2
- package/dist/client/index.d.ts +13 -2
- package/dist/client/index.mjs +129 -29
- package/dist/client/topology.mjs +1 -1
- package/dist/replication/index.d.ts +52 -5
- package/dist/replication/index.mjs +45 -1
- package/dist/transport/grpc.d.ts +1 -1
- package/dist/transport/memory.d.ts +1 -1
- package/dist/{types-DtUwSmAQ.d.ts → types-DxoEm08T.d.ts} +1 -1
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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 };
|
package/dist/client/index.d.ts
CHANGED
|
@@ -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
|
|
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
|
*/
|
package/dist/client/index.mjs
CHANGED
|
@@ -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-
|
|
2
|
-
export { HttpTransport, RemoteDatabase, RemoteError, RemoteLiveQuery, RemoteSubscriptionBuilderImpl, SQL_REFUSED_MESSAGE, ServerCapabilities, WebSocketTransport } from '../chunk-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
1289
|
+
this.setState("running");
|
|
1196
1290
|
} catch (err) {
|
|
1197
1291
|
this.teardownStream();
|
|
1198
|
-
this.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
1432
|
+
this.setError(null);
|
|
1329
1433
|
} catch (err) {
|
|
1330
1434
|
const failure = describeError(err);
|
|
1331
|
-
this.
|
|
1435
|
+
this.setError(failure);
|
|
1332
1436
|
this.resync.recordFailure();
|
|
1333
1437
|
const databaseUsable = await this.snapshotGateOpen(port);
|
|
1334
|
-
this.
|
|
1438
|
+
this.setState("stopped");
|
|
1335
1439
|
try {
|
|
1336
1440
|
await this.start();
|
|
1337
1441
|
} catch {
|
|
1338
|
-
this.
|
|
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.
|
|
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.
|
|
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.
|
|
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 };
|
package/dist/client/topology.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DatabaseClient, toBaseUrl, createEndpointTransport, RemoteError, toServerBaseUrl, unrefTimer } from '../chunk-
|
|
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 };
|
package/dist/transport/grpc.d.ts
CHANGED
|
@@ -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-
|
|
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-
|
|
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,
|
|
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.
|
|
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",
|