@aurora-blue/signalk-connector 0.2.0-dev.20 → 0.2.0-dev.22
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/README.md +1 -1
- package/VERSION +1 -1
- package/dist/src/config.d.ts +2 -2
- package/dist/src/config.js +2 -2
- package/dist/src/connector.d.ts +2 -0
- package/dist/src/connector.js +120 -11
- package/dist/src/state-store.d.ts +5 -1
- package/dist/src/state-store.js +112 -15
- package/dist/src/types.d.ts +15 -2
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.js +1 -1
- package/package.json +1 -1
- package/public/index.html +1 -1
- package/public/locales/de.json +7 -0
- package/public/locales/en.json +7 -0
- package/public/locales/es.json +7 -0
- package/public/locales/fr.json +7 -0
- package/public/status.css +4 -0
- package/public/status.js +30 -6
package/README.md
CHANGED
|
@@ -49,7 +49,7 @@ Configure the plugin in the Signal K administration interface:
|
|
|
49
49
|
|
|
50
50
|
Select Aurora Blue ABOARD Connector Status under Signal K Webapps for the cross-version runtime view. The page refreshes every five seconds and reads a plugin-local endpoint governed by the Signal K server's access controls. The response omits device identity and credentials, and the page does not send data to another host.
|
|
51
51
|
|
|
52
|
-
The connector exchanges the pairing code once and clears it from the Signal K plugin configuration after successful pairing. The returned device token is stored only in the protected connector state file and is never written to logs.
|
|
52
|
+
The connector exchanges the pairing code once and clears it from the Signal K plugin configuration after successful pairing. The returned device token is stored only in the protected connector state file and is never written to logs. The pairing is bound to the configured target URL. Changing that URL requires a new pairing code and pauses collection and transfer until the replacement pairing succeeds. The configuration and status page warn that a successful replacement permanently discards telemetry buffered for the previous server. A failed pairing attempt retains the existing pairing and buffer.
|
|
53
53
|
|
|
54
54
|
The default configuration sends up to 250 events per batch, waits up to five seconds to combine normal telemetry, samples selected own-vessel Signal K paths every second, samples selected AIS paths every five seconds, and enables the preliminary underway change-or-heartbeat profile. AIS can be disabled independently. The `auto` storage profile selects a constrained, standard, or extended byte budget from filesystem capacity while preserving a free-space reserve. Its effective event limit is derived from measured journal bytes per event instead of a fixed 10,000-event default. Protocol 1.1 permits no more than 500 events per batch, and the server continues to accept exact protocol 1.0 backlog batches during upgrades.
|
|
55
55
|
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.2.0-dev.
|
|
1
|
+
0.2.0-dev.22
|
package/dist/src/config.d.ts
CHANGED
|
@@ -35,7 +35,7 @@ export declare const pluginSchema: {
|
|
|
35
35
|
readonly targetUrl: {
|
|
36
36
|
readonly type: "string";
|
|
37
37
|
readonly title: "Aurora Blue target URL";
|
|
38
|
-
readonly description: "Base URL of Aurora Blue Server or Aurora Blue Cloud. [Open connector status](
|
|
38
|
+
readonly description: "Base URL of Aurora Blue ABOARD Server or Aurora Blue ABOARD Cloud. Changing this address requires a new pairing code. [Open connector status](/@aurora-blue/signalk-connector/).";
|
|
39
39
|
};
|
|
40
40
|
readonly deviceName: {
|
|
41
41
|
readonly type: "string";
|
|
@@ -45,7 +45,7 @@ export declare const pluginSchema: {
|
|
|
45
45
|
readonly pairingCode: {
|
|
46
46
|
readonly type: "string";
|
|
47
47
|
readonly title: "Pairing code";
|
|
48
|
-
readonly description: "Short-lived code from Aurora Blue.
|
|
48
|
+
readonly description: "Short-lived code from Aurora Blue ABOARD. Entering a code while already paired changes the server pairing. After the new code is accepted, all data buffered for the previous pairing is permanently discarded. The code is cleared after successful pairing.";
|
|
49
49
|
};
|
|
50
50
|
readonly allowInsecureHttp: {
|
|
51
51
|
readonly type: "boolean";
|
package/dist/src/config.js
CHANGED
|
@@ -154,7 +154,7 @@ exports.pluginSchema = {
|
|
|
154
154
|
targetUrl: {
|
|
155
155
|
type: "string",
|
|
156
156
|
title: "Aurora Blue target URL",
|
|
157
|
-
description: "Base URL of Aurora Blue Server or Aurora Blue Cloud. [Open connector status](
|
|
157
|
+
description: "Base URL of Aurora Blue ABOARD Server or Aurora Blue ABOARD Cloud. Changing this address requires a new pairing code. [Open connector status](/@aurora-blue/signalk-connector/).",
|
|
158
158
|
},
|
|
159
159
|
deviceName: {
|
|
160
160
|
type: "string",
|
|
@@ -164,7 +164,7 @@ exports.pluginSchema = {
|
|
|
164
164
|
pairingCode: {
|
|
165
165
|
type: "string",
|
|
166
166
|
title: "Pairing code",
|
|
167
|
-
description: "Short-lived code from Aurora Blue.
|
|
167
|
+
description: "Short-lived code from Aurora Blue ABOARD. Entering a code while already paired changes the server pairing. After the new code is accepted, all data buffered for the previous pairing is permanently discarded. The code is cleared after successful pairing.",
|
|
168
168
|
},
|
|
169
169
|
allowInsecureHttp: {
|
|
170
170
|
type: "boolean",
|
package/dist/src/connector.d.ts
CHANGED
|
@@ -29,6 +29,7 @@ export declare class ConnectorRuntime {
|
|
|
29
29
|
private droppedEvents;
|
|
30
30
|
private suppressedEvents;
|
|
31
31
|
private transferNotice;
|
|
32
|
+
private pairingState;
|
|
32
33
|
private operatingMode;
|
|
33
34
|
private runtimeConfigurationPollMs;
|
|
34
35
|
private runtimeConfigurationState;
|
|
@@ -60,6 +61,7 @@ export declare class ConnectorRuntime {
|
|
|
60
61
|
private schedule;
|
|
61
62
|
private scheduleRuntimeConfigurationPoll;
|
|
62
63
|
private pollRuntimeConfiguration;
|
|
64
|
+
private requirePairing;
|
|
63
65
|
private applyRuntimeConfiguration;
|
|
64
66
|
private updateStatus;
|
|
65
67
|
private storageBudgetPolicy;
|
package/dist/src/connector.js
CHANGED
|
@@ -39,6 +39,7 @@ class ConnectorRuntime {
|
|
|
39
39
|
droppedEvents = 0;
|
|
40
40
|
suppressedEvents = 0;
|
|
41
41
|
transferNotice = null;
|
|
42
|
+
pairingState = "unpaired";
|
|
42
43
|
operatingMode = "active";
|
|
43
44
|
runtimeConfigurationPollMs = 60_000;
|
|
44
45
|
runtimeConfigurationState = "cached";
|
|
@@ -77,15 +78,53 @@ class ConnectorRuntime {
|
|
|
77
78
|
? this.config.compressionThresholdBytes
|
|
78
79
|
: null);
|
|
79
80
|
let state = await this.store.summary();
|
|
80
|
-
if (
|
|
81
|
+
if (this.config.pairingCode !== undefined) {
|
|
81
82
|
const pairing = await this.client.pair(this.config.pairingCode, this.config.deviceName, version_1.CONNECTOR_VERSION);
|
|
82
|
-
|
|
83
|
+
if (state.device_id === null && state.bufferedEvents === 0) {
|
|
84
|
+
await this.store.setPairing(pairing, this.config.targetUrl);
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
await this.store.replacePairing(pairing, this.config.targetUrl);
|
|
88
|
+
}
|
|
83
89
|
state = await this.store.summary();
|
|
84
90
|
}
|
|
85
91
|
if (state.device_id === null) {
|
|
92
|
+
this.pairingState = "unpaired";
|
|
86
93
|
this.app.setPluginStatus("Unpaired - enter an Aurora Blue pairing code in plugin settings.");
|
|
87
94
|
return;
|
|
88
95
|
}
|
|
96
|
+
if (state.target_url !== null &&
|
|
97
|
+
state.target_url !== this.config.targetUrl) {
|
|
98
|
+
this.pairingState = "pairing-required";
|
|
99
|
+
this.configureSubscriptions();
|
|
100
|
+
this.publishAnchorState(this.anchorAlarm.snapshot());
|
|
101
|
+
this.startSafetyTimer();
|
|
102
|
+
await this.updateStatus();
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (state.target_url === null) {
|
|
106
|
+
this.pairingState = "verifying";
|
|
107
|
+
try {
|
|
108
|
+
const response = await this.client.fetchRuntimeConfiguration(state.access_token);
|
|
109
|
+
await this.store.bindTarget(this.config.targetUrl);
|
|
110
|
+
this.pairingState = "paired";
|
|
111
|
+
this.operatingMode = state.operating_mode;
|
|
112
|
+
await this.applyRuntimeConfiguration(response.runtime_configuration);
|
|
113
|
+
state = await this.store.summary();
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
this.runtimeConfigurationState = "unavailable";
|
|
117
|
+
if (error instanceof http_client_1.AuroraBlueHttpError && error.status === 401) {
|
|
118
|
+
this.pairingState = "pairing-required";
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
this.scheduleRuntimeConfigurationPoll(this.config.retryMinimumMs);
|
|
122
|
+
}
|
|
123
|
+
await this.updateStatus();
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
this.pairingState = "paired";
|
|
89
128
|
this.operatingMode = state.operating_mode;
|
|
90
129
|
this.configureSubscriptions();
|
|
91
130
|
this.publishAnchorState(this.anchorAlarm.snapshot());
|
|
@@ -134,7 +173,9 @@ class ConnectorRuntime {
|
|
|
134
173
|
this.publishAnchorTransition(transition);
|
|
135
174
|
}
|
|
136
175
|
}
|
|
137
|
-
if (this.
|
|
176
|
+
if (this.pairingState !== "paired" ||
|
|
177
|
+
this.operatingMode !== "active" ||
|
|
178
|
+
events.length === 0) {
|
|
138
179
|
return;
|
|
139
180
|
}
|
|
140
181
|
const admitted = events.slice(0, MAXIMUM_PENDING_CAPTURE_EVENTS - this.pendingCaptureEvents);
|
|
@@ -183,8 +224,12 @@ class ConnectorRuntime {
|
|
|
183
224
|
const state = await this.store.summary();
|
|
184
225
|
const emittedPerHour = this.emittedPerHour();
|
|
185
226
|
return {
|
|
186
|
-
paired: state.device_id !== null,
|
|
227
|
+
paired: state.device_id !== null && this.pairingState === "paired",
|
|
228
|
+
pairingState: this.pairingState,
|
|
187
229
|
deviceId: state.device_id,
|
|
230
|
+
pairedTargetUrl: state.target_url,
|
|
231
|
+
lastPairingChangeAt: state.last_pairing_change_at,
|
|
232
|
+
discardedEventsOnLastPairing: state.last_pairing_discarded_events,
|
|
188
233
|
bufferedEvents: state.bufferedEvents,
|
|
189
234
|
acceptedThrough: state.accepted_through,
|
|
190
235
|
lastSuccessAt: state.last_success_at,
|
|
@@ -203,11 +248,13 @@ class ConnectorRuntime {
|
|
|
203
248
|
? null
|
|
204
249
|
: budget.remainingEvents / emittedPerHour,
|
|
205
250
|
operatingMode: state.operating_mode,
|
|
206
|
-
transferState: this.
|
|
207
|
-
? "
|
|
208
|
-
:
|
|
209
|
-
? "
|
|
210
|
-
:
|
|
251
|
+
transferState: this.pairingState !== "paired"
|
|
252
|
+
? "waiting"
|
|
253
|
+
: this.transferNotice !== null
|
|
254
|
+
? "delayed"
|
|
255
|
+
: state.last_success_at === null
|
|
256
|
+
? "waiting"
|
|
257
|
+
: "healthy",
|
|
211
258
|
transferMessage: this.transferNotice,
|
|
212
259
|
runtimeConfigurationState: this.runtimeConfigurationState,
|
|
213
260
|
runtimeConfigurationUpdatedAt: this.runtimeConfigurationUpdatedAt,
|
|
@@ -249,6 +296,10 @@ class ConnectorRuntime {
|
|
|
249
296
|
}
|
|
250
297
|
}
|
|
251
298
|
catch (error) {
|
|
299
|
+
if (error instanceof http_client_1.AuroraBlueHttpError && error.status === 401) {
|
|
300
|
+
await this.requirePairing();
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
252
303
|
this.retryAttempt += 1;
|
|
253
304
|
const delay = (0, backoff_1.retryDelay)(this.retryAttempt, this.config.retryMinimumMs, this.config.retryMaximumMs, this.random);
|
|
254
305
|
const reason = error instanceof http_client_1.AuroraBlueHttpError
|
|
@@ -266,6 +317,12 @@ class ConnectorRuntime {
|
|
|
266
317
|
if (this.operatingMode === "disabled") {
|
|
267
318
|
return;
|
|
268
319
|
}
|
|
320
|
+
if (this.pairingState !== "paired") {
|
|
321
|
+
if (this.config.anchorAlarm.enabled) {
|
|
322
|
+
this.subscribeToContext("vessels.self", ["navigation.position"], this.config.safetySamplePeriodMs);
|
|
323
|
+
}
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
269
326
|
if (this.operatingMode === "standby") {
|
|
270
327
|
if (this.config.anchorAlarm.enabled) {
|
|
271
328
|
this.subscribeToContext("vessels.self", ["navigation.position"], this.config.safetySamplePeriodMs);
|
|
@@ -390,16 +447,62 @@ class ConnectorRuntime {
|
|
|
390
447
|
if (state.access_token === null) {
|
|
391
448
|
return;
|
|
392
449
|
}
|
|
450
|
+
if (state.target_url !== null &&
|
|
451
|
+
state.target_url !== this.config.targetUrl) {
|
|
452
|
+
await this.requirePairing();
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
393
455
|
try {
|
|
394
456
|
const response = await this.client.fetchRuntimeConfiguration(state.access_token);
|
|
457
|
+
if (state.target_url === null) {
|
|
458
|
+
await this.store.bindTarget(this.config.targetUrl);
|
|
459
|
+
}
|
|
460
|
+
this.pairingState = "paired";
|
|
395
461
|
await this.applyRuntimeConfiguration(response.runtime_configuration);
|
|
462
|
+
if (this.unsubscribes.length === 0) {
|
|
463
|
+
this.operatingMode = response.runtime_configuration.operating_mode;
|
|
464
|
+
this.configureSubscriptions();
|
|
465
|
+
this.publishAnchorState(this.anchorAlarm.snapshot());
|
|
466
|
+
this.startSafetyTimer();
|
|
467
|
+
if (this.operatingMode !== "disabled") {
|
|
468
|
+
this.schedule(0);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
396
471
|
}
|
|
397
|
-
catch {
|
|
472
|
+
catch (error) {
|
|
398
473
|
this.runtimeConfigurationState = "unavailable";
|
|
474
|
+
if (error instanceof http_client_1.AuroraBlueHttpError && error.status === 401) {
|
|
475
|
+
await this.requirePairing();
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
399
478
|
}
|
|
400
479
|
await this.updateStatus();
|
|
401
480
|
this.scheduleRuntimeConfigurationPoll(this.runtimeConfigurationPollMs);
|
|
402
481
|
}
|
|
482
|
+
async requirePairing() {
|
|
483
|
+
this.pairingState = "pairing-required";
|
|
484
|
+
this.transferNotice = null;
|
|
485
|
+
this.runtimeConfigurationState = "unavailable";
|
|
486
|
+
if (this.timer !== null) {
|
|
487
|
+
clearTimeout(this.timer);
|
|
488
|
+
this.timer = null;
|
|
489
|
+
}
|
|
490
|
+
if (this.runtimeConfigurationTimer !== null) {
|
|
491
|
+
clearTimeout(this.runtimeConfigurationTimer);
|
|
492
|
+
this.runtimeConfigurationTimer = null;
|
|
493
|
+
}
|
|
494
|
+
if (this.safetyTimer !== null) {
|
|
495
|
+
clearInterval(this.safetyTimer);
|
|
496
|
+
this.safetyTimer = null;
|
|
497
|
+
}
|
|
498
|
+
for (const unsubscribe of this.unsubscribes.splice(0)) {
|
|
499
|
+
unsubscribe();
|
|
500
|
+
}
|
|
501
|
+
this.configureSubscriptions();
|
|
502
|
+
this.publishAnchorState(this.anchorAlarm.snapshot());
|
|
503
|
+
this.startSafetyTimer();
|
|
504
|
+
await this.updateStatus();
|
|
505
|
+
}
|
|
403
506
|
async applyRuntimeConfiguration(configuration) {
|
|
404
507
|
const previousMode = this.operatingMode;
|
|
405
508
|
this.runtimeConfigurationPollMs = configuration.poll_after_seconds * 1_000;
|
|
@@ -431,7 +534,13 @@ class ConnectorRuntime {
|
|
|
431
534
|
? `, suppressed ${status.suppressedEvents}`
|
|
432
535
|
: "";
|
|
433
536
|
const lastSuccess = status.lastSuccessAt ?? "never";
|
|
434
|
-
const prefix = this.
|
|
537
|
+
const prefix = this.pairingState === "pairing-required"
|
|
538
|
+
? "Pairing required - enter a new code; buffered data is discarded after successful pairing"
|
|
539
|
+
: this.pairingState === "verifying"
|
|
540
|
+
? "Verifying pairing"
|
|
541
|
+
: this.pairingState === "unpaired"
|
|
542
|
+
? "Unpaired"
|
|
543
|
+
: (this.transferNotice ?? "Paired");
|
|
435
544
|
const utilization = (status.bufferUtilization * 100).toFixed(1);
|
|
436
545
|
const remaining = status.estimatedRemainingBufferHours === null
|
|
437
546
|
? "estimating remaining time"
|
|
@@ -25,7 +25,9 @@ export declare class StateStore {
|
|
|
25
25
|
}>;
|
|
26
26
|
journalMetrics(): Promise<JournalMetrics>;
|
|
27
27
|
filesystemCapacity(): Promise<FilesystemCapacity | null>;
|
|
28
|
-
setPairing(pairing: PairingResponse): Promise<void>;
|
|
28
|
+
setPairing(pairing: PairingResponse, targetUrl: string): Promise<void>;
|
|
29
|
+
bindTarget(targetUrl: string): Promise<void>;
|
|
30
|
+
replacePairing(pairing: PairingResponse, targetUrl: string): Promise<number>;
|
|
29
31
|
setOperatingMode(operatingMode: OperatingMode): Promise<void>;
|
|
30
32
|
append(event: EventInput, eventLimit: number, byteLimit?: number): Promise<BufferedEvent | null>;
|
|
31
33
|
pendingBatch(maximumEvents: number): Promise<IngestBatch | null>;
|
|
@@ -39,6 +41,8 @@ export declare class StateStore {
|
|
|
39
41
|
private migrateLegacy;
|
|
40
42
|
private preserveLegacyBackup;
|
|
41
43
|
private removeStaleTemporaryFiles;
|
|
44
|
+
private journalDirectory;
|
|
45
|
+
private removeObsoleteJournals;
|
|
42
46
|
private persistMetadata;
|
|
43
47
|
private exists;
|
|
44
48
|
private readFilesystemCapacity;
|
package/dist/src/state-store.js
CHANGED
|
@@ -10,16 +10,21 @@ const METADATA_FILE = "metadata.json";
|
|
|
10
10
|
const LEGACY_STATE_FILE = "state.json";
|
|
11
11
|
const LEGACY_BACKUP_FILE = "state.v1.backup.json";
|
|
12
12
|
const SEGMENT_PATTERN = /^segment-(\d{12})\.jsonl$/;
|
|
13
|
+
const JOURNAL_DIRECTORY_PATTERN = /^journal-([a-zA-Z0-9-]+)$/;
|
|
13
14
|
const initialState = () => ({
|
|
14
|
-
version:
|
|
15
|
+
version: 3,
|
|
15
16
|
protocol_version: types_1.PROTOCOL_VERSION,
|
|
16
17
|
device_id: null,
|
|
17
18
|
access_token: null,
|
|
19
|
+
target_url: null,
|
|
20
|
+
journal_generation: null,
|
|
18
21
|
next_sequence: 1,
|
|
19
22
|
accepted_through: 0,
|
|
20
23
|
events: [],
|
|
21
24
|
pending_batch: null,
|
|
22
25
|
last_success_at: null,
|
|
26
|
+
last_pairing_change_at: null,
|
|
27
|
+
last_pairing_discarded_events: 0,
|
|
23
28
|
operating_mode: "active",
|
|
24
29
|
});
|
|
25
30
|
const isPositiveInteger = (value) => typeof value === "number" && Number.isInteger(value) && value >= 0;
|
|
@@ -27,12 +32,18 @@ const isSupportedProtocolVersion = (value) => typeof value === "string" &&
|
|
|
27
32
|
types_1.SUPPORTED_PROTOCOL_VERSIONS.some((version) => version === value);
|
|
28
33
|
const isOperatingMode = (value) => value === "active" || value === "standby" || value === "disabled";
|
|
29
34
|
const assertState = (state) => {
|
|
30
|
-
if (state.version !==
|
|
35
|
+
if (state.version !== 3 ||
|
|
31
36
|
!isSupportedProtocolVersion(state.protocol_version) ||
|
|
32
37
|
!isPositiveInteger(state.accepted_through) ||
|
|
33
38
|
!isPositiveInteger(state.next_sequence) ||
|
|
34
39
|
state.next_sequence < 1 ||
|
|
35
40
|
!Array.isArray(state.events) ||
|
|
41
|
+
(state.target_url !== null && typeof state.target_url !== "string") ||
|
|
42
|
+
(state.journal_generation !== null &&
|
|
43
|
+
!/^[a-zA-Z0-9-]+$/.test(state.journal_generation)) ||
|
|
44
|
+
(state.last_pairing_change_at !== null &&
|
|
45
|
+
typeof state.last_pairing_change_at !== "string") ||
|
|
46
|
+
!isPositiveInteger(state.last_pairing_discarded_events) ||
|
|
36
47
|
!isOperatingMode(state.operating_mode) ||
|
|
37
48
|
(state.device_id === null) !== (state.access_token === null)) {
|
|
38
49
|
throw new Error("Aurora Blue connector state is invalid.");
|
|
@@ -63,7 +74,11 @@ const assertState = (state) => {
|
|
|
63
74
|
const assertLegacyState = (state) => {
|
|
64
75
|
assertState({
|
|
65
76
|
...state,
|
|
66
|
-
version:
|
|
77
|
+
version: 3,
|
|
78
|
+
target_url: null,
|
|
79
|
+
journal_generation: null,
|
|
80
|
+
last_pairing_change_at: null,
|
|
81
|
+
last_pairing_discarded_events: 0,
|
|
67
82
|
operating_mode: isOperatingMode(state.operating_mode)
|
|
68
83
|
? state.operating_mode
|
|
69
84
|
: "active",
|
|
@@ -72,14 +87,18 @@ const assertLegacyState = (state) => {
|
|
|
72
87
|
const checksum = (event) => (0, node_crypto_1.createHash)("sha256").update(JSON.stringify(event)).digest("hex");
|
|
73
88
|
const encodeRecord = (event) => `${JSON.stringify({ checksum: checksum(event), event })}\n`;
|
|
74
89
|
const metadataFrom = (state) => ({
|
|
75
|
-
version:
|
|
90
|
+
version: 3,
|
|
76
91
|
protocol_version: state.protocol_version,
|
|
77
92
|
device_id: state.device_id,
|
|
78
93
|
access_token: state.access_token,
|
|
94
|
+
target_url: state.target_url,
|
|
95
|
+
journal_generation: state.journal_generation,
|
|
79
96
|
next_sequence: state.next_sequence,
|
|
80
97
|
accepted_through: state.accepted_through,
|
|
81
98
|
pending_batch: state.pending_batch,
|
|
82
99
|
last_success_at: state.last_success_at,
|
|
100
|
+
last_pairing_change_at: state.last_pairing_change_at,
|
|
101
|
+
last_pairing_discarded_events: state.last_pairing_discarded_events,
|
|
83
102
|
operating_mode: state.operating_mode,
|
|
84
103
|
});
|
|
85
104
|
class StateStore {
|
|
@@ -116,7 +135,7 @@ class StateStore {
|
|
|
116
135
|
}
|
|
117
136
|
}
|
|
118
137
|
const metadata = await this.readMetadata();
|
|
119
|
-
const journal = await this.readJournal(metadata.accepted_through);
|
|
138
|
+
const journal = await this.readJournal(metadata.accepted_through, metadata.journal_generation);
|
|
120
139
|
this.state = {
|
|
121
140
|
...metadata,
|
|
122
141
|
protocol_version: types_1.PROTOCOL_VERSION,
|
|
@@ -128,9 +147,8 @@ class StateStore {
|
|
|
128
147
|
await this.removeAcknowledgedSegments(this.state.accepted_through);
|
|
129
148
|
await this.preserveLegacyBackup();
|
|
130
149
|
await this.removeStaleTemporaryFiles();
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
}
|
|
150
|
+
await this.removeObsoleteJournals(metadata.journal_generation);
|
|
151
|
+
await this.persistMetadata();
|
|
134
152
|
}
|
|
135
153
|
async snapshot() {
|
|
136
154
|
await this.tail;
|
|
@@ -155,17 +173,56 @@ class StateStore {
|
|
|
155
173
|
await this.tail;
|
|
156
174
|
return this.capacityProvider();
|
|
157
175
|
}
|
|
158
|
-
async setPairing(pairing) {
|
|
176
|
+
async setPairing(pairing, targetUrl) {
|
|
159
177
|
await this.serialized(async () => {
|
|
160
178
|
if (this.state.device_id !== null || this.state.events.length > 0) {
|
|
161
179
|
throw new Error("Connector is already paired or contains buffered events.");
|
|
162
180
|
}
|
|
163
181
|
this.state.device_id = pairing.device_id;
|
|
164
182
|
this.state.access_token = pairing.access_token;
|
|
183
|
+
this.state.target_url = targetUrl;
|
|
184
|
+
this.state.last_pairing_change_at = this.now();
|
|
185
|
+
this.state.last_pairing_discarded_events = 0;
|
|
165
186
|
this.state.operating_mode = pairing.runtime_configuration.operating_mode;
|
|
166
187
|
await this.persistMetadata();
|
|
167
188
|
});
|
|
168
189
|
}
|
|
190
|
+
async bindTarget(targetUrl) {
|
|
191
|
+
await this.serialized(async () => {
|
|
192
|
+
if (this.state.device_id === null ||
|
|
193
|
+
this.state.access_token === null ||
|
|
194
|
+
this.state.target_url !== null) {
|
|
195
|
+
throw new Error("Connector target binding is not available.");
|
|
196
|
+
}
|
|
197
|
+
this.state.target_url = targetUrl;
|
|
198
|
+
await this.persistMetadata();
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
async replacePairing(pairing, targetUrl) {
|
|
202
|
+
let discardedEvents = 0;
|
|
203
|
+
await this.serialized(async () => {
|
|
204
|
+
discardedEvents = this.state.events.length;
|
|
205
|
+
const journalGeneration = this.uuid();
|
|
206
|
+
const nextJournalDirectory = this.journalDirectory(journalGeneration);
|
|
207
|
+
await (0, promises_1.mkdir)(nextJournalDirectory, { recursive: false, mode: 0o700 });
|
|
208
|
+
await (0, promises_1.chmod)(nextJournalDirectory, 0o700);
|
|
209
|
+
this.state = {
|
|
210
|
+
...initialState(),
|
|
211
|
+
device_id: pairing.device_id,
|
|
212
|
+
access_token: pairing.access_token,
|
|
213
|
+
target_url: targetUrl,
|
|
214
|
+
journal_generation: journalGeneration,
|
|
215
|
+
last_pairing_change_at: this.now(),
|
|
216
|
+
last_pairing_discarded_events: discardedEvents,
|
|
217
|
+
operating_mode: pairing.runtime_configuration.operating_mode,
|
|
218
|
+
};
|
|
219
|
+
this.segments = [];
|
|
220
|
+
this.journalBytes = 0;
|
|
221
|
+
await this.persistMetadata();
|
|
222
|
+
await this.removeObsoleteJournals(journalGeneration).catch(() => undefined);
|
|
223
|
+
});
|
|
224
|
+
return discardedEvents;
|
|
225
|
+
}
|
|
169
226
|
async setOperatingMode(operatingMode) {
|
|
170
227
|
await this.serialized(async () => {
|
|
171
228
|
this.state.operating_mode = operatingMode;
|
|
@@ -262,7 +319,7 @@ class StateStore {
|
|
|
262
319
|
if (segment === undefined ||
|
|
263
320
|
(segment.bytes > 0 &&
|
|
264
321
|
segment.bytes + payloadBytes > this.segmentMaximumBytes)) {
|
|
265
|
-
const path = (0, node_path_1.join)(this.
|
|
322
|
+
const path = (0, node_path_1.join)(this.journalDirectory(this.state.journal_generation), `segment-${String(event.sequence).padStart(12, "0")}.jsonl`);
|
|
266
323
|
segment = {
|
|
267
324
|
path,
|
|
268
325
|
firstSequence: event.sequence,
|
|
@@ -297,22 +354,41 @@ class StateStore {
|
|
|
297
354
|
const parsedMetadata = parsed;
|
|
298
355
|
const metadata = {
|
|
299
356
|
...parsedMetadata,
|
|
357
|
+
version: 3,
|
|
358
|
+
target_url: parsedMetadata.version === 3 ? parsedMetadata.target_url : null,
|
|
359
|
+
journal_generation: parsedMetadata.version === 3 ? parsedMetadata.journal_generation : null,
|
|
360
|
+
last_pairing_change_at: parsedMetadata.version === 3
|
|
361
|
+
? parsedMetadata.last_pairing_change_at
|
|
362
|
+
: null,
|
|
363
|
+
last_pairing_discarded_events: parsedMetadata.version === 3
|
|
364
|
+
? parsedMetadata.last_pairing_discarded_events
|
|
365
|
+
: 0,
|
|
300
366
|
operating_mode: isOperatingMode(parsedMetadata.operating_mode)
|
|
301
367
|
? parsedMetadata.operating_mode
|
|
302
368
|
: "active",
|
|
303
369
|
};
|
|
304
|
-
if (
|
|
370
|
+
if (![2, 3].includes(parsedMetadata.version) ||
|
|
305
371
|
!isSupportedProtocolVersion(metadata.protocol_version) ||
|
|
306
372
|
!isPositiveInteger(metadata.accepted_through) ||
|
|
307
373
|
!isPositiveInteger(metadata.next_sequence) ||
|
|
308
374
|
!isOperatingMode(metadata.operating_mode) ||
|
|
375
|
+
(metadata.target_url !== null &&
|
|
376
|
+
typeof metadata.target_url !== "string") ||
|
|
377
|
+
(metadata.journal_generation !== null &&
|
|
378
|
+
!/^[a-zA-Z0-9-]+$/.test(metadata.journal_generation)) ||
|
|
379
|
+
(metadata.last_pairing_change_at !== null &&
|
|
380
|
+
typeof metadata.last_pairing_change_at !== "string") ||
|
|
381
|
+
!isPositiveInteger(metadata.last_pairing_discarded_events) ||
|
|
309
382
|
(metadata.device_id === null) !== (metadata.access_token === null)) {
|
|
310
383
|
throw new Error("Aurora Blue connector metadata is invalid.");
|
|
311
384
|
}
|
|
312
385
|
return metadata;
|
|
313
386
|
}
|
|
314
|
-
async readJournal(acceptedThrough) {
|
|
315
|
-
const
|
|
387
|
+
async readJournal(acceptedThrough, journalGeneration) {
|
|
388
|
+
const directory = this.journalDirectory(journalGeneration);
|
|
389
|
+
await (0, promises_1.mkdir)(directory, { recursive: true, mode: 0o700 });
|
|
390
|
+
await (0, promises_1.chmod)(directory, 0o700);
|
|
391
|
+
const names = (await (0, promises_1.readdir)(directory))
|
|
316
392
|
.filter((name) => SEGMENT_PATTERN.test(name))
|
|
317
393
|
.sort();
|
|
318
394
|
const events = [];
|
|
@@ -323,7 +399,7 @@ class StateStore {
|
|
|
323
399
|
if (match === null) {
|
|
324
400
|
continue;
|
|
325
401
|
}
|
|
326
|
-
const path = (0, node_path_1.join)(
|
|
402
|
+
const path = (0, node_path_1.join)(directory, name);
|
|
327
403
|
const segmentEvents = await this.readSegment(path);
|
|
328
404
|
const details = await (0, promises_1.stat)(path);
|
|
329
405
|
const firstSequence = Number(match[1]);
|
|
@@ -404,8 +480,12 @@ class StateStore {
|
|
|
404
480
|
this.journalBytes = 0;
|
|
405
481
|
this.state = {
|
|
406
482
|
...legacy,
|
|
407
|
-
version:
|
|
483
|
+
version: 3,
|
|
408
484
|
protocol_version: types_1.PROTOCOL_VERSION,
|
|
485
|
+
target_url: null,
|
|
486
|
+
journal_generation: null,
|
|
487
|
+
last_pairing_change_at: null,
|
|
488
|
+
last_pairing_discarded_events: 0,
|
|
409
489
|
operating_mode: isOperatingMode(legacy.operating_mode)
|
|
410
490
|
? legacy.operating_mode
|
|
411
491
|
: "active",
|
|
@@ -431,6 +511,23 @@ class StateStore {
|
|
|
431
511
|
}
|
|
432
512
|
}
|
|
433
513
|
}
|
|
514
|
+
journalDirectory(generation) {
|
|
515
|
+
return generation === null
|
|
516
|
+
? this.directory
|
|
517
|
+
: (0, node_path_1.join)(this.directory, `journal-${generation}`);
|
|
518
|
+
}
|
|
519
|
+
async removeObsoleteJournals(currentGeneration) {
|
|
520
|
+
const currentDirectory = currentGeneration === null ? null : `journal-${currentGeneration}`;
|
|
521
|
+
for (const name of await (0, promises_1.readdir)(this.directory)) {
|
|
522
|
+
if (SEGMENT_PATTERN.test(name) && currentGeneration !== null) {
|
|
523
|
+
await (0, promises_1.unlink)((0, node_path_1.join)(this.directory, name));
|
|
524
|
+
continue;
|
|
525
|
+
}
|
|
526
|
+
if (JOURNAL_DIRECTORY_PATTERN.test(name) && name !== currentDirectory) {
|
|
527
|
+
await (0, promises_1.rm)((0, node_path_1.join)(this.directory, name), { recursive: true, force: true });
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
434
531
|
async persistMetadata() {
|
|
435
532
|
const temporaryPath = (0, node_path_1.join)(this.directory, `.metadata-${this.uuid()}.tmp`);
|
|
436
533
|
const payload = `${JSON.stringify(metadataFrom(this.state))}\n`;
|
package/dist/src/types.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ export declare const PROTOCOL_VERSION: "1.1";
|
|
|
3
3
|
export declare const SUPPORTED_PROTOCOL_VERSIONS: readonly ["1.0", "1.1"];
|
|
4
4
|
export type ProtocolVersion = (typeof SUPPORTED_PROTOCOL_VERSIONS)[number];
|
|
5
5
|
export type OperatingMode = "active" | "standby" | "disabled";
|
|
6
|
+
export type PairingState = "unpaired" | "verifying" | "paired" | "pairing-required";
|
|
6
7
|
export interface RuntimeConfiguration {
|
|
7
8
|
version: number;
|
|
8
9
|
operating_mode: OperatingMode;
|
|
@@ -67,31 +68,43 @@ export interface RuntimeConfigurationResponse {
|
|
|
67
68
|
server_time: string;
|
|
68
69
|
}
|
|
69
70
|
export interface PersistedState {
|
|
70
|
-
version:
|
|
71
|
+
version: 3;
|
|
71
72
|
protocol_version: ProtocolVersion;
|
|
72
73
|
device_id: string | null;
|
|
73
74
|
access_token: string | null;
|
|
75
|
+
target_url: string | null;
|
|
76
|
+
journal_generation: string | null;
|
|
74
77
|
next_sequence: number;
|
|
75
78
|
accepted_through: number;
|
|
76
79
|
events: BufferedEvent[];
|
|
77
80
|
pending_batch: IngestBatch | null;
|
|
78
81
|
last_success_at: string | null;
|
|
82
|
+
last_pairing_change_at: string | null;
|
|
83
|
+
last_pairing_discarded_events: number;
|
|
79
84
|
operating_mode: OperatingMode;
|
|
80
85
|
}
|
|
81
86
|
export interface PersistedMetadata {
|
|
82
|
-
version:
|
|
87
|
+
version: 3;
|
|
83
88
|
protocol_version: ProtocolVersion;
|
|
84
89
|
device_id: string | null;
|
|
85
90
|
access_token: string | null;
|
|
91
|
+
target_url: string | null;
|
|
92
|
+
journal_generation: string | null;
|
|
86
93
|
next_sequence: number;
|
|
87
94
|
accepted_through: number;
|
|
88
95
|
pending_batch: IngestBatch | null;
|
|
89
96
|
last_success_at: string | null;
|
|
97
|
+
last_pairing_change_at: string | null;
|
|
98
|
+
last_pairing_discarded_events: number;
|
|
90
99
|
operating_mode: OperatingMode;
|
|
91
100
|
}
|
|
92
101
|
export interface ConnectorStatus {
|
|
93
102
|
paired: boolean;
|
|
103
|
+
pairingState: PairingState;
|
|
94
104
|
deviceId: string | null;
|
|
105
|
+
pairedTargetUrl: string | null;
|
|
106
|
+
lastPairingChangeAt: string | null;
|
|
107
|
+
discardedEventsOnLastPairing: number;
|
|
95
108
|
bufferedEvents: number;
|
|
96
109
|
acceptedThrough: number;
|
|
97
110
|
lastSuccessAt: string | null;
|
package/dist/src/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const CONNECTOR_VERSION = "0.2.0-dev.
|
|
1
|
+
export declare const CONNECTOR_VERSION = "0.2.0-dev.22";
|
package/dist/src/version.js
CHANGED
package/package.json
CHANGED
package/public/index.html
CHANGED
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
aria-label="Connector summary"
|
|
73
73
|
data-i18n-aria-label="summary.label"
|
|
74
74
|
>
|
|
75
|
-
<article class="card">
|
|
75
|
+
<article id="pairing-card" class="card">
|
|
76
76
|
<span class="label" data-i18n="summary.pairing">Pairing</span>
|
|
77
77
|
<strong id="pairing" data-i18n="status.unknown">Unknown</strong>
|
|
78
78
|
<span
|
package/public/locales/de.json
CHANGED
|
@@ -38,6 +38,9 @@
|
|
|
38
38
|
"status.updated": "Aktualisiert {time}",
|
|
39
39
|
"status.paired": "Gekoppelt",
|
|
40
40
|
"status.unpaired": "Nicht gekoppelt",
|
|
41
|
+
"status.pairing_required_detail": "Geben Sie in der Konfiguration einen neuen Kopplungscode ein. Nach erfolgreicher Kopplung wird der Offline-Puffer des vorherigen Servers dauerhaft gelöscht.",
|
|
42
|
+
"status.pairing_verifying_detail": "Die bestehende Kopplung wird für diesen Server geprüft.",
|
|
43
|
+
"status.pairing_discarded": "Beim letzten Serverwechsel wurden {count} gepufferte Ereignisse gelöscht.",
|
|
41
44
|
"status.runtime_configuration": "Laufzeitkonfiguration {state}",
|
|
42
45
|
"status.last_success": "Letzter Erfolg: {time}",
|
|
43
46
|
"status.events": "{current} / {limit} Ereignisse",
|
|
@@ -57,6 +60,10 @@
|
|
|
57
60
|
"value.configuration.cached": "zwischengespeichert",
|
|
58
61
|
"value.configuration.current": "aktuell",
|
|
59
62
|
"value.configuration.unavailable": "nicht verfügbar",
|
|
63
|
+
"value.pairing.unpaired": "Nicht gekoppelt",
|
|
64
|
+
"value.pairing.verifying": "Wird geprüft",
|
|
65
|
+
"value.pairing.paired": "Gekoppelt",
|
|
66
|
+
"value.pairing.pairing_required": "Kopplung erforderlich",
|
|
60
67
|
"value.transfer.waiting": "Wartet",
|
|
61
68
|
"value.transfer.healthy": "Fehlerfrei",
|
|
62
69
|
"value.transfer.delayed": "Verzögert",
|
package/public/locales/en.json
CHANGED
|
@@ -38,6 +38,9 @@
|
|
|
38
38
|
"status.updated": "Updated {time}",
|
|
39
39
|
"status.paired": "Paired",
|
|
40
40
|
"status.unpaired": "Unpaired",
|
|
41
|
+
"status.pairing_required_detail": "Enter a new pairing code in the configuration. After successful pairing, the offline buffer for the previous server is permanently discarded.",
|
|
42
|
+
"status.pairing_verifying_detail": "The existing pairing is being verified for this server.",
|
|
43
|
+
"status.pairing_discarded": "The last server change discarded {count} buffered events.",
|
|
41
44
|
"status.runtime_configuration": "Runtime configuration {state}",
|
|
42
45
|
"status.last_success": "Last success: {time}",
|
|
43
46
|
"status.events": "{current} / {limit} events",
|
|
@@ -57,6 +60,10 @@
|
|
|
57
60
|
"value.configuration.cached": "cached",
|
|
58
61
|
"value.configuration.current": "current",
|
|
59
62
|
"value.configuration.unavailable": "unavailable",
|
|
63
|
+
"value.pairing.unpaired": "Unpaired",
|
|
64
|
+
"value.pairing.verifying": "Verifying",
|
|
65
|
+
"value.pairing.paired": "Paired",
|
|
66
|
+
"value.pairing.pairing_required": "Pairing Required",
|
|
60
67
|
"value.transfer.waiting": "Waiting",
|
|
61
68
|
"value.transfer.healthy": "Healthy",
|
|
62
69
|
"value.transfer.delayed": "Delayed",
|
package/public/locales/es.json
CHANGED
|
@@ -38,6 +38,9 @@
|
|
|
38
38
|
"status.updated": "Actualizado {time}",
|
|
39
39
|
"status.paired": "Emparejado",
|
|
40
40
|
"status.unpaired": "No emparejado",
|
|
41
|
+
"status.pairing_required_detail": "Introduzca un nuevo código de emparejamiento en la configuración. Después de emparejar correctamente, el búfer sin conexión del servidor anterior se elimina de forma permanente.",
|
|
42
|
+
"status.pairing_verifying_detail": "Se está verificando el emparejamiento existente para este servidor.",
|
|
43
|
+
"status.pairing_discarded": "El último cambio de servidor eliminó {count} eventos almacenados en el búfer.",
|
|
41
44
|
"status.runtime_configuration": "Configuración de ejecución {state}",
|
|
42
45
|
"status.last_success": "Último éxito: {time}",
|
|
43
46
|
"status.events": "{current} / {limit} eventos",
|
|
@@ -57,6 +60,10 @@
|
|
|
57
60
|
"value.configuration.cached": "en caché",
|
|
58
61
|
"value.configuration.current": "actual",
|
|
59
62
|
"value.configuration.unavailable": "no disponible",
|
|
63
|
+
"value.pairing.unpaired": "No emparejado",
|
|
64
|
+
"value.pairing.verifying": "Verificando",
|
|
65
|
+
"value.pairing.paired": "Emparejado",
|
|
66
|
+
"value.pairing.pairing_required": "Emparejamiento necesario",
|
|
60
67
|
"value.transfer.waiting": "Esperando",
|
|
61
68
|
"value.transfer.healthy": "Correcta",
|
|
62
69
|
"value.transfer.delayed": "Retrasada",
|
package/public/locales/fr.json
CHANGED
|
@@ -38,6 +38,9 @@
|
|
|
38
38
|
"status.updated": "Mis à jour {time}",
|
|
39
39
|
"status.paired": "Appairé",
|
|
40
40
|
"status.unpaired": "Non appairé",
|
|
41
|
+
"status.pairing_required_detail": "Saisissez un nouveau code d'appairage dans la configuration. Après un appairage réussi, le tampon hors ligne de l'ancien serveur est définitivement supprimé.",
|
|
42
|
+
"status.pairing_verifying_detail": "L'appairage existant est en cours de vérification pour ce serveur.",
|
|
43
|
+
"status.pairing_discarded": "Le dernier changement de serveur a supprimé {count} événements mis en mémoire tampon.",
|
|
41
44
|
"status.runtime_configuration": "Configuration d'exécution {state}",
|
|
42
45
|
"status.last_success": "Dernier succès : {time}",
|
|
43
46
|
"status.events": "{current} / {limit} événements",
|
|
@@ -57,6 +60,10 @@
|
|
|
57
60
|
"value.configuration.cached": "en cache",
|
|
58
61
|
"value.configuration.current": "actuelle",
|
|
59
62
|
"value.configuration.unavailable": "indisponible",
|
|
63
|
+
"value.pairing.unpaired": "Non appairé",
|
|
64
|
+
"value.pairing.verifying": "Vérification",
|
|
65
|
+
"value.pairing.paired": "Appairé",
|
|
66
|
+
"value.pairing.pairing_required": "Appairage requis",
|
|
60
67
|
"value.transfer.waiting": "En attente",
|
|
61
68
|
"value.transfer.healthy": "Opérationnel",
|
|
62
69
|
"value.transfer.delayed": "Retardé",
|
package/public/status.css
CHANGED
package/public/status.js
CHANGED
|
@@ -119,13 +119,37 @@ const render = (payload) => {
|
|
|
119
119
|
translatedValue("mode", status.operatingMode),
|
|
120
120
|
toneForMode(status.operatingMode),
|
|
121
121
|
);
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
122
|
+
const pairingState =
|
|
123
|
+
status.pairingState ?? (status.paired ? "paired" : "unpaired");
|
|
124
|
+
const pairingCard = element("pairing-card");
|
|
125
|
+
pairingCard.classList.toggle(
|
|
126
|
+
"attention",
|
|
127
|
+
pairingState === "pairing-required",
|
|
128
128
|
);
|
|
129
|
+
setText("pairing", translatedValue("pairing", pairingState));
|
|
130
|
+
|
|
131
|
+
if (pairingState === "pairing-required") {
|
|
132
|
+
setText("configuration-state", t("status.pairing_required_detail"));
|
|
133
|
+
} else if (pairingState === "verifying") {
|
|
134
|
+
setText("configuration-state", t("status.pairing_verifying_detail"));
|
|
135
|
+
} else if (status.discardedEventsOnLastPairing > 0) {
|
|
136
|
+
setText(
|
|
137
|
+
"configuration-state",
|
|
138
|
+
t("status.pairing_discarded", {
|
|
139
|
+
count: number.format(status.discardedEventsOnLastPairing),
|
|
140
|
+
}),
|
|
141
|
+
);
|
|
142
|
+
} else {
|
|
143
|
+
setText(
|
|
144
|
+
"configuration-state",
|
|
145
|
+
t("status.runtime_configuration", {
|
|
146
|
+
state: translatedValue(
|
|
147
|
+
"configuration",
|
|
148
|
+
status.runtimeConfigurationState,
|
|
149
|
+
),
|
|
150
|
+
}),
|
|
151
|
+
);
|
|
152
|
+
}
|
|
129
153
|
setText("transfer", translatedValue("transfer", status.transferState));
|
|
130
154
|
const delayed = status.transferMessage?.match(
|
|
131
155
|
/^Transfer delayed \((HTTP (\d+)|target unavailable)\) - retry in (\d+)s$/,
|