@dxos/async 0.8.4-staging.ac66bdf99f → 0.9.0
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 +102 -5
- package/dist/lib/browser/index.mjs +28 -85
- package/dist/lib/browser/index.mjs.map +3 -3
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +28 -85
- package/dist/lib/node-esm/index.mjs.map +3 -3
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/types/src/callback.d.ts.map +1 -1
- package/dist/types/src/chain.d.ts.map +1 -1
- package/dist/types/src/cleanup.d.ts.map +1 -1
- package/dist/types/src/debounce.d.ts.map +1 -1
- package/dist/types/src/errors.d.ts.map +1 -1
- package/dist/types/src/event-emitter.d.ts.map +1 -1
- package/dist/types/src/events.d.ts.map +1 -1
- package/dist/types/src/mutex.d.ts.map +1 -1
- package/dist/types/src/observable-value.d.ts.map +1 -1
- package/dist/types/src/observable.d.ts.map +1 -1
- package/dist/types/src/persistent-lifecycle.d.ts +1 -0
- package/dist/types/src/persistent-lifecycle.d.ts.map +1 -1
- package/dist/types/src/stream-to-array.d.ts.map +1 -1
- package/dist/types/src/task-scheduling.d.ts.map +1 -1
- package/dist/types/src/test-stream.d.ts.map +1 -1
- package/dist/types/src/testing.d.ts.map +1 -1
- package/dist/types/src/timeout.d.ts.map +1 -1
- package/dist/types/src/timer.d.ts.map +1 -1
- package/dist/types/src/track-leaks.d.ts.map +1 -1
- package/dist/types/src/trigger.d.ts.map +1 -1
- package/dist/types/src/update-scheduler.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +8 -11
- package/src/persistent-lifecycle.test.ts +36 -0
- package/src/persistent-lifecycle.ts +31 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dxos/async",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Async utilities.",
|
|
5
5
|
"homepage": "https://dxos.org",
|
|
6
6
|
"bugs": "https://github.com/dxos/dxos/issues",
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"type": "git",
|
|
9
9
|
"url": "https://github.com/dxos/dxos"
|
|
10
10
|
},
|
|
11
|
-
"license": "
|
|
11
|
+
"license": "FSL-1.1-Apache-2.0",
|
|
12
12
|
"author": "DXOS.org",
|
|
13
13
|
"sideEffects": true,
|
|
14
14
|
"type": "module",
|
|
@@ -23,9 +23,6 @@
|
|
|
23
23
|
}
|
|
24
24
|
},
|
|
25
25
|
"types": "dist/types/src/index.d.ts",
|
|
26
|
-
"typesVersions": {
|
|
27
|
-
"*": {}
|
|
28
|
-
},
|
|
29
26
|
"files": [
|
|
30
27
|
"dist",
|
|
31
28
|
"src"
|
|
@@ -33,12 +30,12 @@
|
|
|
33
30
|
"dependencies": {
|
|
34
31
|
"zen-observable": "^0.10.0",
|
|
35
32
|
"zen-push": "^0.3.1",
|
|
36
|
-
"@dxos/context": "0.
|
|
37
|
-
"@dxos/
|
|
38
|
-
"@dxos/
|
|
39
|
-
"@dxos/
|
|
40
|
-
"@dxos/node-std": "0.
|
|
41
|
-
"@dxos/util": "0.
|
|
33
|
+
"@dxos/context": "0.9.0",
|
|
34
|
+
"@dxos/log": "0.9.0",
|
|
35
|
+
"@dxos/invariant": "0.9.0",
|
|
36
|
+
"@dxos/debug": "0.9.0",
|
|
37
|
+
"@dxos/node-std": "0.9.0",
|
|
38
|
+
"@dxos/util": "0.9.0"
|
|
42
39
|
},
|
|
43
40
|
"devDependencies": {
|
|
44
41
|
"@types/zen-observable": "^0.8.3"
|
|
@@ -57,6 +57,42 @@ describe('ConnectionState', () => {
|
|
|
57
57
|
expect(timeToTrigger).to.be.greaterThanOrEqual(100);
|
|
58
58
|
});
|
|
59
59
|
|
|
60
|
+
test('connection that drops immediately backs off instead of hot-looping', async () => {
|
|
61
|
+
const startTimes: number[] = [];
|
|
62
|
+
const maxStarts = 4;
|
|
63
|
+
const done = new Trigger();
|
|
64
|
+
|
|
65
|
+
const persistentLifecycle = new PersistentLifecycle({
|
|
66
|
+
start: async () => {
|
|
67
|
+
startTimes.push(Date.now());
|
|
68
|
+
if (startTimes.length >= maxStarts) {
|
|
69
|
+
done.wake();
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
stop: async () => {},
|
|
73
|
+
// Simulate the connection dropping the moment it is established.
|
|
74
|
+
onRestart: async () => {
|
|
75
|
+
if (startTimes.length < maxStarts) {
|
|
76
|
+
void persistentLifecycle.scheduleRestart();
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
await persistentLifecycle.open();
|
|
82
|
+
onTestFinished(async () => {
|
|
83
|
+
await persistentLifecycle.close();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// The initial open already performed the first start; simulate its immediate drop.
|
|
87
|
+
void persistentLifecycle.scheduleRestart();
|
|
88
|
+
await done.wait({ timeout: 5000 });
|
|
89
|
+
|
|
90
|
+
// Successive reconnects must back off (~0, ~100, ~200ms), not fire back-to-back.
|
|
91
|
+
const gaps = startTimes.slice(1).map((time, index) => time - startTimes[index]);
|
|
92
|
+
expect(gaps[1]).to.be.greaterThanOrEqual(90);
|
|
93
|
+
expect(gaps[2]).to.be.greaterThanOrEqual(180);
|
|
94
|
+
});
|
|
95
|
+
|
|
60
96
|
test('finish `restart` before close', async () => {
|
|
61
97
|
let restarted = false;
|
|
62
98
|
const persistentLifecycle = new PersistentLifecycle({
|
|
@@ -13,6 +13,13 @@ import { sleep } from './timeout';
|
|
|
13
13
|
const INIT_RESTART_DELAY = 100;
|
|
14
14
|
const DEFAULT_MAX_RESTART_DELAY = 5000;
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Minimum duration a connection must stay up before it is considered stable and the backoff is
|
|
18
|
+
* reset. A connection that drops sooner keeps escalating the delay, so an endpoint that accepts
|
|
19
|
+
* then immediately closes the connection cannot produce a zero-delay reconnect loop.
|
|
20
|
+
*/
|
|
21
|
+
const STABLE_CONNECTION_THRESHOLD = 5000;
|
|
22
|
+
|
|
16
23
|
export type PersistentLifecycleProps<T> = {
|
|
17
24
|
/**
|
|
18
25
|
* Create connection.
|
|
@@ -50,6 +57,7 @@ export class PersistentLifecycle<T> extends Resource {
|
|
|
50
57
|
private _currentState: T | undefined = undefined;
|
|
51
58
|
private _restartTask?: DeferredTask = undefined;
|
|
52
59
|
private _restartAfter = 0;
|
|
60
|
+
private _connectedAt: number | undefined = undefined;
|
|
53
61
|
|
|
54
62
|
constructor({ start, stop, onRestart, maxRestartDelay = DEFAULT_MAX_RESTART_DELAY }: PersistentLifecycleProps<T>) {
|
|
55
63
|
super();
|
|
@@ -69,16 +77,26 @@ export class PersistentLifecycle<T> extends Resource {
|
|
|
69
77
|
try {
|
|
70
78
|
await this._restart();
|
|
71
79
|
} catch (err) {
|
|
80
|
+
// Suppress noise from restarts that race with shutdown.
|
|
81
|
+
if (this._ctx?.disposed) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
72
84
|
log.warn('Restart failed', { err });
|
|
73
85
|
this._restartTask?.schedule();
|
|
74
86
|
}
|
|
75
87
|
});
|
|
76
88
|
|
|
77
|
-
|
|
89
|
+
try {
|
|
90
|
+
this._currentState = await this._start();
|
|
91
|
+
this._connectedAt = Date.now();
|
|
92
|
+
} catch (err) {
|
|
93
|
+
// Suppress noise when shutdown was requested while the initial start was in flight.
|
|
94
|
+
if (this._ctx?.disposed) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
78
97
|
log.warn('Start failed', { err });
|
|
79
98
|
this._restartTask?.schedule();
|
|
80
|
-
|
|
81
|
-
});
|
|
99
|
+
}
|
|
82
100
|
}
|
|
83
101
|
|
|
84
102
|
protected override async _close(): Promise<void> {
|
|
@@ -89,6 +107,15 @@ export class PersistentLifecycle<T> extends Resource {
|
|
|
89
107
|
|
|
90
108
|
private async _restart(): Promise<void> {
|
|
91
109
|
log(`restarting in ${this._restartAfter}ms`, { state: this._lifecycleState });
|
|
110
|
+
|
|
111
|
+
// Reset the backoff only if the previous connection stayed up long enough to be considered
|
|
112
|
+
// stable. A connection that drops shortly after starting must keep escalating the delay,
|
|
113
|
+
// otherwise reconnects degenerate into a hot loop.
|
|
114
|
+
if (this._connectedAt !== undefined && Date.now() - this._connectedAt >= STABLE_CONNECTION_THRESHOLD) {
|
|
115
|
+
this._restartAfter = 0;
|
|
116
|
+
}
|
|
117
|
+
this._connectedAt = undefined;
|
|
118
|
+
|
|
92
119
|
await this._stopCurrentState();
|
|
93
120
|
if (this._lifecycleState !== LifecycleState.OPEN) {
|
|
94
121
|
return;
|
|
@@ -101,7 +128,7 @@ export class PersistentLifecycle<T> extends Resource {
|
|
|
101
128
|
this._currentState = await this._start();
|
|
102
129
|
});
|
|
103
130
|
|
|
104
|
-
this.
|
|
131
|
+
this._connectedAt = Date.now();
|
|
105
132
|
await this._onRestart?.();
|
|
106
133
|
}
|
|
107
134
|
|