@harperfast/harper-pro 5.0.17 → 5.0.18
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/core/resources/RecordEncoder.ts +15 -12
- package/core/resources/RocksTransactionLogStore.ts +51 -0
- package/core/resources/Table.ts +17 -15
- package/core/resources/auditStore.ts +97 -5
- package/core/resources/databases.ts +67 -7
- package/dist/cloneNode/cloneNode.js +13 -8
- package/dist/cloneNode/cloneNode.js.map +1 -1
- package/dist/core/resources/RecordEncoder.js +1 -1
- package/dist/core/resources/RecordEncoder.js.map +1 -1
- package/dist/core/resources/RocksTransactionLogStore.js.map +1 -1
- package/dist/core/resources/Table.js +17 -17
- package/dist/core/resources/Table.js.map +1 -1
- package/dist/core/resources/auditStore.js +82 -5
- package/dist/core/resources/auditStore.js.map +1 -1
- package/dist/core/resources/databases.js +68 -5
- package/dist/core/resources/databases.js.map +1 -1
- package/dist/replication/replicationConnection.js +63 -18
- package/dist/replication/replicationConnection.js.map +1 -1
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/replication/replicationConnection.ts +66 -20
- package/studio/web/assets/{index-DhLu-DHX.js → index-Tv7e9k8K.js} +5 -5
- package/studio/web/assets/{index-DhLu-DHX.js.map → index-Tv7e9k8K.js.map} +1 -1
- package/studio/web/assets/{index.lazy-DBjOisCz.js → index.lazy-De4JGuec.js} +2 -2
- package/studio/web/assets/{index.lazy-DBjOisCz.js.map → index.lazy-De4JGuec.js.map} +1 -1
- package/studio/web/assets/{profile-DSL-499E.js → profile-voeNsl4C.js} +2 -2
- package/studio/web/assets/{profile-DSL-499E.js.map → profile-voeNsl4C.js.map} +1 -1
- package/studio/web/assets/{status-BRW5QtzY.js → status-110CCE-v.js} +2 -2
- package/studio/web/assets/{status-BRW5QtzY.js.map → status-110CCE-v.js.map} +1 -1
- package/studio/web/index.html +1 -1
|
@@ -206,6 +206,11 @@ export class NodeReplicationConnection extends EventEmitter {
|
|
|
206
206
|
retries = 0;
|
|
207
207
|
isConnected = true; // we start out assuming we will be connected
|
|
208
208
|
isFinished = false;
|
|
209
|
+
// Set when this connection should never reconnect: user-driven unsubscribe(), or the
|
|
210
|
+
// empty-subscription delayed close inside replicateOverWS. Distinct from `isFinished`,
|
|
211
|
+
// which is the post-close terminal marker. Anything else (protocol errors, peer
|
|
212
|
+
// DISCONNECT, etc.) leaves this false so the close handler schedules a retry.
|
|
213
|
+
intentionallyUnsubscribed = false;
|
|
209
214
|
nodeSubscriptions?: NodeSubscription[];
|
|
210
215
|
latency = 0;
|
|
211
216
|
replicateTablesByDefault: boolean;
|
|
@@ -248,18 +253,36 @@ export class NodeReplicationConnection extends EventEmitter {
|
|
|
248
253
|
});
|
|
249
254
|
}
|
|
250
255
|
this.isConnected = true;
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
256
|
+
try {
|
|
257
|
+
session = replicateOverWS(
|
|
258
|
+
this.socket,
|
|
259
|
+
{
|
|
260
|
+
database: this.databaseName,
|
|
261
|
+
subscription: this.subscription,
|
|
262
|
+
url: this.url,
|
|
263
|
+
connection: this,
|
|
264
|
+
isSubscriptionConnection: this.nodeSubscriptions !== undefined,
|
|
265
|
+
},
|
|
266
|
+
{ replicates: true } // pre-authorized, but should only make publish: true if we are allowing reverse subscriptions
|
|
267
|
+
);
|
|
268
|
+
this.sessionResolve(session);
|
|
269
|
+
} catch (error) {
|
|
270
|
+
// replicateOverWS does a fair amount of synchronous setup (setDatabase, audit
|
|
271
|
+
// store wiring, ping bookkeeping) and any of it can throw — most worryingly,
|
|
272
|
+
// audit decoder corruption surfaced via setDatabase or the immediate getRange.
|
|
273
|
+
// Without this guard the throw escapes the WS 'open' listener as an
|
|
274
|
+
// uncaughtException, the socket stays open, sessionResolve is never called, no
|
|
275
|
+
// 'close' fires, and the retry timer in the close handler never gets scheduled —
|
|
276
|
+
// leaving the (peer, db) pair stuck with `connected: false` on main but no further
|
|
277
|
+
// activity until a process restart. Terminating the socket forces the close
|
|
278
|
+
// handler to run, which now retries.
|
|
279
|
+
logger.error?.(
|
|
280
|
+
`Error setting up replication session to ${this.url} (db: "${this.databaseName}"), terminating to retry`,
|
|
281
|
+
error
|
|
282
|
+
);
|
|
283
|
+
this.sessionReject(error);
|
|
284
|
+
this.socket.terminate();
|
|
285
|
+
}
|
|
263
286
|
});
|
|
264
287
|
this.socket.on('error', (error) => {
|
|
265
288
|
if (error.code === 'SELF_SIGNED_CERT_IN_CHAIN') {
|
|
@@ -277,21 +300,27 @@ export class NodeReplicationConnection extends EventEmitter {
|
|
|
277
300
|
this.sessionReject(error);
|
|
278
301
|
});
|
|
279
302
|
this.socket.on('close', (code, reasonBuffer) => {
|
|
280
|
-
//
|
|
303
|
+
// Only treat the close as terminal when something explicitly marked it as a deliberate
|
|
304
|
+
// teardown (user unsubscribe or the empty-subscription delayed close). Protocol-level
|
|
305
|
+
// closes — peer DISCONNECT, unauthorized after open, node-name-mismatch, invalid
|
|
306
|
+
// sequence id, etc. — used to also set `isFinished` via replicateOverWS's close()
|
|
307
|
+
// helper, which left the connection silently dead and required hdb_nodes churn to
|
|
308
|
+
// recover. Those now fall through to the retry path below.
|
|
309
|
+
const intentional = this.intentionallyUnsubscribed;
|
|
281
310
|
if (this.isConnected) {
|
|
282
311
|
if (this.nodeSubscriptions) {
|
|
283
312
|
disconnectedFromNode({
|
|
284
313
|
name: this.nodeName,
|
|
285
314
|
database: this.databaseName,
|
|
286
315
|
url: this.url,
|
|
287
|
-
finished:
|
|
316
|
+
finished: intentional,
|
|
288
317
|
});
|
|
289
318
|
}
|
|
290
319
|
this.isConnected = false;
|
|
291
320
|
}
|
|
292
321
|
this.removeAllListeners('subscriptions-updated');
|
|
293
322
|
|
|
294
|
-
if (
|
|
323
|
+
if (intentional) {
|
|
295
324
|
this.isFinished = true;
|
|
296
325
|
session?.end();
|
|
297
326
|
this.emit('finished');
|
|
@@ -327,7 +356,7 @@ export class NodeReplicationConnection extends EventEmitter {
|
|
|
327
356
|
this.emit('subscriptions-updated', nodeSubscriptions);
|
|
328
357
|
}
|
|
329
358
|
unsubscribe() {
|
|
330
|
-
this.
|
|
359
|
+
this.intentionallyUnsubscribed = true;
|
|
331
360
|
this.socket.close(1008, 'No longer subscribed');
|
|
332
361
|
}
|
|
333
362
|
|
|
@@ -1100,11 +1129,20 @@ export function replicateOverWS(ws: WebSocket, options: any, authorization: Prom
|
|
|
1100
1129
|
const table = tableEntry.table;
|
|
1101
1130
|
const primaryStore = table.primaryStore;
|
|
1102
1131
|
const encoder = primaryStore.encoder;
|
|
1132
|
+
// Force a reload the first time this connection touches each table:
|
|
1133
|
+
// `primaryStore.encoder` is a process-wide singleton, so its typedStructs
|
|
1134
|
+
// may have been populated to a stale length by prior activity on this
|
|
1135
|
+
// thread, and this connection's initial TABLE_FIXED_STRUCTURE must reflect
|
|
1136
|
+
// what's actually in LMDB right now. After this, HAS_STRUCTURE_UPDATE on
|
|
1137
|
+
// subsequent audit records keeps the encoder in sync, since every
|
|
1138
|
+
// typed-struct addition produces a flagged audit record.
|
|
1103
1139
|
if (
|
|
1140
|
+
!tableEntry.structuresLoaded ||
|
|
1104
1141
|
auditRecord.extendedType & HAS_STRUCTURE_UPDATE ||
|
|
1105
1142
|
!encoder.typedStructs ||
|
|
1106
1143
|
auditRecord.structureVersion > encoder.typedStructs.length + encoder.structures.length
|
|
1107
1144
|
) {
|
|
1145
|
+
tableEntry.structuresLoaded = true;
|
|
1108
1146
|
// there is a structure update, we need to reload the structure from storage.
|
|
1109
1147
|
// this is copied from msgpackr's struct, may want to expose as public method
|
|
1110
1148
|
encoder._mergeStructures(encoder.getStructures());
|
|
@@ -1770,12 +1808,19 @@ export function replicateOverWS(ws: WebSocket, options: any, authorization: Prom
|
|
|
1770
1808
|
logger.debug?.(connectionId, 'closed', code, reasonBuffer?.toString());
|
|
1771
1809
|
});
|
|
1772
1810
|
|
|
1773
|
-
function close(code?, reason
|
|
1811
|
+
function close(code?, reason?, intentional?: boolean) {
|
|
1774
1812
|
try {
|
|
1775
|
-
|
|
1813
|
+
// Only the deliberate "we are done with this connection" call sites pass intentional=true
|
|
1814
|
+
// (currently just the empty-subscription delayed close below). Everything else — auth
|
|
1815
|
+
// failures after open, peer-initiated DISCONNECT, schema/sequence errors — is a
|
|
1816
|
+
// transient protocol close that should reconnect, so we let the WS close event fall
|
|
1817
|
+
// through to NodeReplicationConnection's normal retry path instead of marking the
|
|
1818
|
+
// connection as finished or emitting 'finished' (which would remove it from the
|
|
1819
|
+
// worker's connections map).
|
|
1820
|
+
if (intentional && options.connection) options.connection.intentionallyUnsubscribed = true;
|
|
1776
1821
|
logger.debug?.(connectionId, 'closing', remoteNodeName, databaseName, code, reason);
|
|
1777
1822
|
ws.close(code, reason);
|
|
1778
|
-
options.connection?.emit('finished'); //
|
|
1823
|
+
if (intentional) options.connection?.emit('finished'); // synchronously indicate that the connection is finished, so it is not accidentally reused
|
|
1779
1824
|
} catch (error) {
|
|
1780
1825
|
logger.error?.(connectionId, 'Error closing connection', error);
|
|
1781
1826
|
}
|
|
@@ -2110,7 +2155,8 @@ export function replicateOverWS(ws: WebSocket, options: any, authorization: Prom
|
|
|
2110
2155
|
const scheduled = performance.now();
|
|
2111
2156
|
delayedClose = setTimeout(() => {
|
|
2112
2157
|
// if we have not received any messages in a while, we can close the connection
|
|
2113
|
-
if (lastMessageTime <= scheduled)
|
|
2158
|
+
if (lastMessageTime <= scheduled)
|
|
2159
|
+
close(1008, 'Connection has no subscriptions and is no longer used', true);
|
|
2114
2160
|
else scheduleClose();
|
|
2115
2161
|
}, DELAY_CLOSE_TIME).unref();
|
|
2116
2162
|
};
|