signalk-chiplog 2.11.0 → 2.12.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/CHANGELOG.md +14 -1
- package/README.md +12 -0
- package/index.js +3 -0
- package/lib/plotter-extension.js +59 -0
- package/package.json +2 -1
- package/public/entry/entry.css +26 -0
- package/public/entry/js/components/AccessGate.mjs +1 -1
- package/public/entry/js/components/CrewDialog.mjs +2 -1
- package/public/entry/js/components/StatusHeader.mjs +1 -1
- package/public/entry/js/confirm.mjs +42 -0
- package/public/entry/js/main.mjs +43 -2
- package/public/entry/js/plotter.mjs +30 -0
- package/public/entry/sw.js +4 -1
- package/public/vendor/plotterext-LICENSE +21 -0
- package/public/vendor/plotterext.mjs +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,18 @@ All notable changes to Chiplog are documented here. The format follows
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [2.12.0] - 2026-10-24
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **Helm entry beside the chart.** Chartplotters supporting the Plotter Extensions API can open Chiplog from a toolbar
|
|
14
|
+
button. The iframe panel keeps drafts while hidden and follows the plotter's night mode when available. The standalone
|
|
15
|
+
tablet app and its offline queue remain available.
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- Event and crew deletion confirmations work inside sandboxed plotter panels.
|
|
20
|
+
|
|
9
21
|
## [2.11.0] - 2026-10-24
|
|
10
22
|
|
|
11
23
|
### Added
|
|
@@ -483,7 +495,8 @@ First release.
|
|
|
483
495
|
- REST API under `/plugins/signalk-chiplog/api`, documented in [docs/API.md](docs/API.md).
|
|
484
496
|
- Single SQLite database through Node's built-in `node:sqlite`: no native module to build.
|
|
485
497
|
|
|
486
|
-
[Unreleased]: https://github.com/ricard33/signalk-chiplog/compare/v2.
|
|
498
|
+
[Unreleased]: https://github.com/ricard33/signalk-chiplog/compare/v2.12.0...HEAD
|
|
499
|
+
[2.12.0]: https://github.com/ricard33/signalk-chiplog/compare/v2.11.0...v2.12.0
|
|
487
500
|
[2.11.0]: https://github.com/ricard33/signalk-chiplog/compare/v2.10.0...v2.11.0
|
|
488
501
|
[2.10.0]: https://github.com/ricard33/signalk-chiplog/compare/v2.9.0...v2.10.0
|
|
489
502
|
[2.9.0]: https://github.com/ricard33/signalk-chiplog/compare/v2.8.0...v2.9.0
|
package/README.md
CHANGED
|
@@ -437,6 +437,18 @@ Logging needs read/write access. The first time, the app shows **This tablet nee
|
|
|
437
437
|
To revoke it, delete the device under **Security → Devices**: the tablet asks for access again. **Sign in instead** uses
|
|
438
438
|
a regular Signal K user account.
|
|
439
439
|
|
|
440
|
+
### From a chartplotter
|
|
441
|
+
|
|
442
|
+
In a chartplotter supporting the Signal K Plotter Extensions API (such as Freeboard-SK), the **Chiplog** toolbar button
|
|
443
|
+
opens the helm entry app in an iframe panel beside the chart. Closing and reopening it keeps notes and handwriting in
|
|
444
|
+
progress; reloading the plotter still discards unsent drafts. The usual offline queue and Signal K access rights apply.
|
|
445
|
+
The panel follows the plotter's night mode when available, and its night switch also changes the plotter's mode.
|
|
446
|
+
Standalone tablet use remains available at the same address.
|
|
447
|
+
|
|
448
|
+
The extension is advertised automatically while the plugin is enabled. A server without the resource-provider API can
|
|
449
|
+
still run the standalone apps. Journal and login navigation links are hidden in the extension: sign in through the
|
|
450
|
+
chartplotter, or use the panel's device access request.
|
|
451
|
+
|
|
440
452
|
## Configuration 🔧
|
|
441
453
|
|
|
442
454
|
In the Signal K admin, **Apps & Plugins → Configuration → Chiplog**.
|
package/index.js
CHANGED
|
@@ -8,6 +8,7 @@ const { INFLUX_DEFAULTS } = require('./lib/influx-history');
|
|
|
8
8
|
const { createLandmarkFinder, LANDMARK_DEFAULTS } = require('./lib/landmark-finder');
|
|
9
9
|
const { OBSERVATION_DEFAULTS } = require('./lib/observation-recorder');
|
|
10
10
|
const { createPlaceNamer, GEOCODING_DEFAULTS } = require('./lib/place-names');
|
|
11
|
+
const { registerPlotterExtension } = require('./lib/plotter-extension');
|
|
11
12
|
const { PROPULSION_DEFAULTS } = require('./lib/propulsion-detector');
|
|
12
13
|
const { createReplayJob } = require('./lib/replay-job');
|
|
13
14
|
const { createSummaryMailer, SUMMARY_MAIL_DEFAULTS } = require('./lib/summary-mailer');
|
|
@@ -706,6 +707,8 @@ module.exports = function (app) {
|
|
|
706
707
|
throw err;
|
|
707
708
|
}
|
|
708
709
|
|
|
710
|
+
registerPlotterExtension(app, () => database !== null);
|
|
711
|
+
|
|
709
712
|
const readSelfPath = (path) => app.getSelfPath(path);
|
|
710
713
|
detector = createPassageDetector({ db: database, readSelfPath, settings });
|
|
711
714
|
const recorder = createTrackRecorder({ db: database, readSelfPath, settings });
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
const { version } = require('../package.json');
|
|
2
|
+
|
|
3
|
+
const EXTENSION_ID = 'signalk-chiplog';
|
|
4
|
+
|
|
5
|
+
function buildManifest() {
|
|
6
|
+
return {
|
|
7
|
+
name: 'Chiplog — Helm entry',
|
|
8
|
+
description: 'Log manoeuvres, notes and handwriting without leaving the chart.',
|
|
9
|
+
version,
|
|
10
|
+
apiVersion: '1',
|
|
11
|
+
requires: ['panels.iframe', 'buttons'],
|
|
12
|
+
optional: ['nightMode'],
|
|
13
|
+
panels: [
|
|
14
|
+
{
|
|
15
|
+
id: 'helm-entry',
|
|
16
|
+
title: 'Chiplog',
|
|
17
|
+
type: 'iframe',
|
|
18
|
+
url: '/signalk-chiplog/entry/?plotterExtension=1',
|
|
19
|
+
lifecycle: 'keepAlive'
|
|
20
|
+
}
|
|
21
|
+
],
|
|
22
|
+
buttons: [
|
|
23
|
+
{
|
|
24
|
+
id: 'open-helm-entry',
|
|
25
|
+
title: 'Chiplog',
|
|
26
|
+
slot: 'mapToolbar',
|
|
27
|
+
icon: 'edit_note',
|
|
28
|
+
action: { type: 'togglePanel', panel: 'helm-entry' }
|
|
29
|
+
}
|
|
30
|
+
]
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function registerPlotterExtension(app, isRunning) {
|
|
35
|
+
// Older servers can still run the standalone logbook and tablet app.
|
|
36
|
+
if (typeof app.registerResourceProvider !== 'function') {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
app.registerResourceProvider({
|
|
40
|
+
type: 'plotterExtensions',
|
|
41
|
+
methods: {
|
|
42
|
+
listResources: async () => (isRunning() ? { [EXTENSION_ID]: buildManifest() } : {}),
|
|
43
|
+
getResource: async (id) => {
|
|
44
|
+
if (!isRunning() || id !== EXTENSION_ID) {
|
|
45
|
+
throw new Error(`No such plotterExtensions resource: ${id}`);
|
|
46
|
+
}
|
|
47
|
+
return buildManifest();
|
|
48
|
+
},
|
|
49
|
+
setResource: async () => {
|
|
50
|
+
throw new Error('Chiplog plotter extension is read-only');
|
|
51
|
+
},
|
|
52
|
+
deleteResource: async () => {
|
|
53
|
+
throw new Error('Chiplog plotter extension is read-only');
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
module.exports = { registerPlotterExtension };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "signalk-chiplog",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.12.0",
|
|
4
4
|
"description": "Nautical logbook for Signal K: automatic entries, handwritten notes",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"files": [
|
|
@@ -81,6 +81,7 @@
|
|
|
81
81
|
"mediabunny": "1.58.0",
|
|
82
82
|
"preact": "^10.29.8",
|
|
83
83
|
"prettier": "^3.9.6",
|
|
84
|
+
"signalk-plotterext-bus": "^0.21.0",
|
|
84
85
|
"three": "0.186.0"
|
|
85
86
|
}
|
|
86
87
|
}
|
package/public/entry/entry.css
CHANGED
|
@@ -749,6 +749,32 @@ input {
|
|
|
749
749
|
border-radius: var(--radius);
|
|
750
750
|
}
|
|
751
751
|
|
|
752
|
+
.confirmation-dialog {
|
|
753
|
+
width: min(40rem, calc(100% - 2rem));
|
|
754
|
+
max-height: calc(100% - 2rem);
|
|
755
|
+
padding: 0;
|
|
756
|
+
border: 0;
|
|
757
|
+
border-radius: var(--radius);
|
|
758
|
+
background: var(--surface);
|
|
759
|
+
color: var(--text);
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
.confirmation-dialog::backdrop {
|
|
763
|
+
background: rgb(0 0 0 / 55%);
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
.confirmation-dialog .sheet {
|
|
767
|
+
width: 100%;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
:root[data-plotter-extension='true'] [data-standalone-link] {
|
|
771
|
+
display: none;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
:root[data-plotter-extension='true'] .entry-main {
|
|
775
|
+
padding: 0.75rem 0.75rem 9rem;
|
|
776
|
+
}
|
|
777
|
+
|
|
752
778
|
.sail-grid {
|
|
753
779
|
display: grid;
|
|
754
780
|
grid-template-columns: repeat(auto-fill, minmax(9rem, 1fr));
|
|
@@ -63,7 +63,7 @@ export function AccessGate({ onGranted }) {
|
|
|
63
63
|
${t('access.request')}
|
|
64
64
|
</button>`
|
|
65
65
|
}
|
|
66
|
-
<a class="big-button" href="/admin/#/login">${t('access.signIn')}</a>
|
|
66
|
+
<a class="big-button" data-standalone-link href="/admin/#/login">${t('access.signIn')}</a>
|
|
67
67
|
</div>
|
|
68
68
|
</section>
|
|
69
69
|
`;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { html, useState } from '../../../vendor/preact-htm.mjs';
|
|
2
2
|
import { useLocale } from '../../../js/context.mjs';
|
|
3
3
|
import { PencilIcon, TrashIcon } from './Icons.mjs';
|
|
4
|
+
import { confirmAction } from '../confirm.mjs';
|
|
4
5
|
|
|
5
6
|
// .big-button is a row flex container, so a bare <br/> between its children
|
|
6
7
|
// would not stack them -- wrap name and role in their own column instead.
|
|
@@ -83,7 +84,7 @@ export function CrewDialog({
|
|
|
83
84
|
};
|
|
84
85
|
|
|
85
86
|
const removeMember = async (member) => {
|
|
86
|
-
if (!
|
|
87
|
+
if (!(await confirmAction(t('entry.deleteCrewMemberConfirm', { name: member.name }), t))) {
|
|
87
88
|
return;
|
|
88
89
|
}
|
|
89
90
|
const removed = await onDeleteMember(member.id);
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Native confirm() is blocked in plotter-extension sandboxes. An HTML dialog
|
|
2
|
+
// works there, traps focus, supports Escape, and keeps the same confirmation
|
|
3
|
+
// in the standalone app. Text is never interpreted as HTML.
|
|
4
|
+
export function confirmAction(message, t) {
|
|
5
|
+
return new Promise((resolve) => {
|
|
6
|
+
const dialog = document.createElement('dialog');
|
|
7
|
+
dialog.className = 'confirmation-dialog';
|
|
8
|
+
dialog.setAttribute('aria-label', message);
|
|
9
|
+
const form = document.createElement('form');
|
|
10
|
+
form.method = 'dialog';
|
|
11
|
+
form.className = 'sheet';
|
|
12
|
+
const text = document.createElement('p');
|
|
13
|
+
text.textContent = message;
|
|
14
|
+
const actions = document.createElement('div');
|
|
15
|
+
actions.className = 'sheet-row';
|
|
16
|
+
for (const [value, label, className] of [
|
|
17
|
+
['cancel', t('common.cancel'), 'big-button'],
|
|
18
|
+
['confirm', t('common.delete'), 'big-button danger']
|
|
19
|
+
]) {
|
|
20
|
+
const button = document.createElement('button');
|
|
21
|
+
button.type = 'submit';
|
|
22
|
+
button.value = value;
|
|
23
|
+
button.textContent = label;
|
|
24
|
+
button.className = className;
|
|
25
|
+
button.autofocus = value === 'cancel';
|
|
26
|
+
actions.append(button);
|
|
27
|
+
}
|
|
28
|
+
form.append(text, actions);
|
|
29
|
+
dialog.append(form);
|
|
30
|
+
dialog.addEventListener(
|
|
31
|
+
'close',
|
|
32
|
+
() => {
|
|
33
|
+
const confirmed = dialog.returnValue === 'confirm';
|
|
34
|
+
dialog.remove();
|
|
35
|
+
resolve(confirmed);
|
|
36
|
+
},
|
|
37
|
+
{ once: true }
|
|
38
|
+
);
|
|
39
|
+
document.body.append(dialog);
|
|
40
|
+
dialog.showModal();
|
|
41
|
+
});
|
|
42
|
+
}
|
package/public/entry/js/main.mjs
CHANGED
|
@@ -12,6 +12,8 @@ import { randomId } from '../../js/ids.mjs';
|
|
|
12
12
|
import { pickLanguage } from '../../js/i18n.mjs';
|
|
13
13
|
import { createServerClock } from './clock.mjs';
|
|
14
14
|
import { createJournal } from './journal.mjs';
|
|
15
|
+
import { confirmAction } from './confirm.mjs';
|
|
16
|
+
import { connectPlotter, isPlotterExtension } from './plotter.mjs';
|
|
15
17
|
import { AccessGate } from './components/AccessGate.mjs';
|
|
16
18
|
import { EventDialog, Toast } from './components/Dialogs.mjs';
|
|
17
19
|
import { CrewDialog } from './components/CrewDialog.mjs';
|
|
@@ -106,6 +108,7 @@ function App({ journal }) {
|
|
|
106
108
|
const [managingCrew, setManagingCrew] = useState(false);
|
|
107
109
|
const [tab, setTab] = useState('note');
|
|
108
110
|
const [night, setNight] = useState(readSetting(NIGHT_KEY) === 'on');
|
|
111
|
+
const [plotter, setPlotter] = useState(null);
|
|
109
112
|
const [version, setVersion] = useState(0);
|
|
110
113
|
const [busy, setBusy] = useState(false);
|
|
111
114
|
|
|
@@ -163,6 +166,43 @@ function App({ journal }) {
|
|
|
163
166
|
writeSetting(NIGHT_KEY, night ? 'on' : 'off');
|
|
164
167
|
}, [night]);
|
|
165
168
|
|
|
169
|
+
useEffect(() => {
|
|
170
|
+
if (!isPlotterExtension) {
|
|
171
|
+
return undefined;
|
|
172
|
+
}
|
|
173
|
+
let active = true;
|
|
174
|
+
let connection;
|
|
175
|
+
connectPlotter((enabled) => active && setNight(enabled))
|
|
176
|
+
.then((client) => {
|
|
177
|
+
if (!active) {
|
|
178
|
+
client.close();
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
connection = client;
|
|
182
|
+
setPlotter(client);
|
|
183
|
+
})
|
|
184
|
+
// The tablet remains usable if the host cannot complete the handshake.
|
|
185
|
+
.catch(() => {});
|
|
186
|
+
return () => {
|
|
187
|
+
active = false;
|
|
188
|
+
connection?.close();
|
|
189
|
+
};
|
|
190
|
+
}, []);
|
|
191
|
+
|
|
192
|
+
const toggleNight = async () => {
|
|
193
|
+
const enabled = !night;
|
|
194
|
+
if (plotter?.hasCapability('nightMode')) {
|
|
195
|
+
try {
|
|
196
|
+
await plotter.call('nightMode.set', { enabled, auto: false });
|
|
197
|
+
setNight(enabled);
|
|
198
|
+
return;
|
|
199
|
+
} catch {
|
|
200
|
+
// Keep the local night switch usable if the host stops answering.
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
setNight(enabled);
|
|
204
|
+
};
|
|
205
|
+
|
|
166
206
|
useEffect(() => {
|
|
167
207
|
if (!toast || toast.kind === 'error') {
|
|
168
208
|
return undefined;
|
|
@@ -307,7 +347,7 @@ function App({ journal }) {
|
|
|
307
347
|
};
|
|
308
348
|
|
|
309
349
|
const deleteEvent = async (event) => {
|
|
310
|
-
if (!
|
|
350
|
+
if (!(await confirmAction(t('entry.deleteConfirm'), t))) {
|
|
311
351
|
return;
|
|
312
352
|
}
|
|
313
353
|
try {
|
|
@@ -376,7 +416,7 @@ function App({ journal }) {
|
|
|
376
416
|
onReconnect=${reconnect}
|
|
377
417
|
queued=${queue.pending.length}
|
|
378
418
|
night=${night}
|
|
379
|
-
onToggleNight=${
|
|
419
|
+
onToggleNight=${toggleNight}
|
|
380
420
|
/>
|
|
381
421
|
<main class="entry-main">
|
|
382
422
|
${
|
|
@@ -498,6 +538,7 @@ if ('serviceWorker' in navigator && window.isSecureContext) {
|
|
|
498
538
|
}
|
|
499
539
|
|
|
500
540
|
const locale = createLocale(pickLanguage(navigator.languages, location.search));
|
|
541
|
+
document.documentElement.dataset.plotterExtension = String(isPlotterExtension);
|
|
501
542
|
document.documentElement.lang = locale.language;
|
|
502
543
|
document.title = locale.t('entry.title');
|
|
503
544
|
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { connectExtension, windowPort } from '../../vendor/plotterext.mjs';
|
|
2
|
+
|
|
3
|
+
// An explicit opt-in leaves standalone and ordinary iframe use unchanged.
|
|
4
|
+
export const isPlotterExtension =
|
|
5
|
+
window.parent !== window && new URLSearchParams(location.search).get('plotterExtension') === '1';
|
|
6
|
+
|
|
7
|
+
export async function connectPlotter(onNightChange) {
|
|
8
|
+
const client = await connectExtension({
|
|
9
|
+
port: windowPort(window.parent, { origin: location.origin }),
|
|
10
|
+
timeoutMs: 5000
|
|
11
|
+
});
|
|
12
|
+
try {
|
|
13
|
+
if (client.hasCapability('nightMode')) {
|
|
14
|
+
// Subscribe first so a change during the initial read cannot be lost.
|
|
15
|
+
let changed = false;
|
|
16
|
+
await client.subscribe(['nightMode.changed'], (_name, state) => {
|
|
17
|
+
changed = true;
|
|
18
|
+
onNightChange(state.enabled);
|
|
19
|
+
});
|
|
20
|
+
const state = await client.call('nightMode.get');
|
|
21
|
+
if (!changed) {
|
|
22
|
+
onNightChange(state.enabled);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return client;
|
|
26
|
+
} catch (error) {
|
|
27
|
+
client.close();
|
|
28
|
+
throw error;
|
|
29
|
+
}
|
|
30
|
+
}
|
package/public/entry/sw.js
CHANGED
|
@@ -13,6 +13,8 @@ const SHELL = [
|
|
|
13
13
|
'icons/icon-512.png',
|
|
14
14
|
'icons/apple-touch-icon.png',
|
|
15
15
|
'js/main.mjs',
|
|
16
|
+
'js/confirm.mjs',
|
|
17
|
+
'js/plotter.mjs',
|
|
16
18
|
'js/access.mjs',
|
|
17
19
|
'js/clock.mjs',
|
|
18
20
|
'js/journal.mjs',
|
|
@@ -43,7 +45,8 @@ const SHELL = [
|
|
|
43
45
|
'../js/view-memory.mjs',
|
|
44
46
|
'../js/components/common.mjs',
|
|
45
47
|
'../js/components/Timeline.mjs',
|
|
46
|
-
'../vendor/preact-htm.mjs'
|
|
48
|
+
'../vendor/preact-htm.mjs',
|
|
49
|
+
'../vendor/plotterext.mjs'
|
|
47
50
|
];
|
|
48
51
|
|
|
49
52
|
const shellUrls = () => new Set(SHELL.map((file) => new URL(file, self.registration.scope).href));
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Joel Kozikowski
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var f="plotterExt/1",o={PARSE_ERROR:-32700,INVALID_REQUEST:-32600,METHOD_NOT_FOUND:-32601,INVALID_PARAMS:-32602,INTERNAL_ERROR:-32603,HOST_ERROR:-32e3,TIMEOUT:-32001,CONNECTION_CLOSED:-32002},c=class l extends Error{code;data;constructor(e,s={}){super(e),this.name="RpcError",this.code=s.code??o.HOST_ERROR;let n={...s.data??{}};s.reason!==void 0&&(n.reason=s.reason),this.data=Object.keys(n).length>0?n:void 0}get reason(){return typeof this.data?.reason=="string"?this.data.reason:void 0}toErrorObject(){return{code:this.code,message:this.message,...this.data?{data:this.data}:{}}}static fromErrorObject(e){return new l(e.message,{code:e.code,data:e.data})}static from(e){return e instanceof l?e:e instanceof Error?new l(e.message,{code:o.INTERNAL_ERROR}):new l(String(e),{code:o.INTERNAL_ERROR})}},u="bus.ready",y="bus.handshake";function E(t,e){return t===e?!0:m(t.split("."),0,e.split("."),0)}function m(t,e,s,n){for(;e<t.length;){let i=t[e];if(i==="**"){if(e===t.length-1)return!0;for(let r=n;r<=s.length;r++)if(m(t,e+1,s,r))return!0;return!1}if(n>=s.length||i!=="*"&&i!==s[n])return!1;e++,n++}return n===s.length}function g(t,e){for(let s of t)if(E(s,e))return!0;return!1}function N(t){return{bus:f,msg:t}}function M(t){if(typeof t!="object"||t===null)return null;let e=t;return e.bus!==f?null:_(e.msg)?e.msg:null}function _(t){if(typeof t!="object"||t===null)return!1;let e=t;if(e.jsonrpc!=="2.0")return!1;if(typeof e.method=="string")return e.id===void 0||typeof e.id=="string"||typeof e.id=="number";if(!(typeof e.id=="string"||typeof e.id=="number"||e.id===null))return!1;let n="result"in e,i=e.error,r=typeof i=="object"&&i!==null&&typeof i.code=="number"&&typeof i.message=="string";return n?!("error"in e):r}function S(t){return"method"in t&&"id"in t&&t.id!==void 0}function A(t){return"method"in t&&(!("id"in t)||t.id===void 0)}function D(t){return!("method"in t)}function h(t,e={}){let s=e.listenWindow??globalThis,n=e.origin??s.location?.origin??"*";return{post(i,r){r?.length?t.postMessage(i,{targetOrigin:n,transfer:r}):t.postMessage(i,n)},listen(i){let r=a=>{a.source===t&&(n!=="*"&&a.origin!==n||i(a.data))};return s.addEventListener("message",r),()=>s.removeEventListener("message",r)}}}var L=1e4,p=class{callTimeoutMs;port;unlisten;onError;pending=new Map;methods=new Map;eventHandlers=new Set;idPrefix=Math.random().toString(36).slice(2,8);seq=0;closed=!1;constructor(t){this.port=t.port,this.callTimeoutMs=t.callTimeoutMs??L,this.onError=t.onError??(e=>console.warn("[plotterext-bus]",e)),this.unlisten=this.port.listen(e=>this.onData(e))}registerMethod(t,e){this.methods.set(t,e)}unregisterMethod(t){this.methods.delete(t)}onEvent(t,e){let s={patterns:t,fn:e};return this.eventHandlers.add(s),()=>this.eventHandlers.delete(s)}notify(t,e,s={}){this.send({jsonrpc:"2.0",method:t,...e!==void 0?{params:e}:{}},s.transfer)}call(t,e,s={}){if(this.closed)return Promise.reject(new c("Bus endpoint is closed",{code:o.CONNECTION_CLOSED,reason:"CLOSED"}));let n=`${this.idPrefix}-${++this.seq}`,i=s.timeoutMs??this.callTimeoutMs;return new Promise((r,a)=>{let d=i>0?setTimeout(()=>{this.pending.delete(n),a(new c(`Call timed out after ${i}ms: ${t}`,{code:o.TIMEOUT,reason:"TIMEOUT"}))},i):null;this.pending.set(n,{resolve:r,reject:a,timer:d}),this.send({jsonrpc:"2.0",id:n,method:t,...e!==void 0?{params:e}:{}},s.transfer)})}close(){if(!this.closed){this.closed=!0,this.unlisten();for(let[,t]of this.pending)t.timer&&clearTimeout(t.timer),t.reject(new c("Bus endpoint closed",{code:o.CONNECTION_CLOSED,reason:"CLOSED"}));this.pending.clear(),this.eventHandlers.clear()}}send(t,e){this.closed||this.port.post(N(t),e)}onData(t){let e=M(t);e&&(D(e)?this.onResponse(e):S(e)?this.onRequest(e):A(e)&&this.onNotification(e.method,e.params))}onResponse(t){if(t.id===null)return;let e=this.pending.get(t.id);e&&(this.pending.delete(t.id),e.timer&&clearTimeout(e.timer),"error"in t?e.reject(c.fromErrorObject(t.error)):e.resolve(t.result))}async onRequest(t){let e=this.methods.get(t.method);if(!e){this.send({jsonrpc:"2.0",id:t.id,error:{code:o.METHOD_NOT_FOUND,message:`Method not found: ${t.method}`}});return}try{let s=await e(t.params,{endpoint:this});this.send({jsonrpc:"2.0",id:t.id,result:s===void 0?null:s})}catch(s){this.send({jsonrpc:"2.0",id:t.id,error:c.from(s).toErrorObject()})}}onNotification(t,e){for(let s of[...this.eventHandlers])if(g(s.patterns,t))try{s.fn(t,e)}catch(n){this.onError(n)}}};var v=class{handshake;endpoint;constructor(t,e){this.endpoint=t,this.handshake=e}get context(){return this.handshake.context}get apiVersion(){return this.handshake.apiVersion}get capabilities(){return this.handshake.capabilities}hasCapability(t){return this.handshake.capabilities.includes(t)}call(t,e,s){return this.endpoint.call(t,e,s)}notify(t,e){this.endpoint.notify(t,e)}async subscribe(t,e){let s=this.endpoint.onEvent(t,e),n;try{n=(await this.call("events.subscribe",{patterns:t})).subscriptionId}catch(i){throw s(),i}return async()=>{s(),await this.call("events.unsubscribe",{subscriptionId:n}).catch(()=>{})}}async publish(t,e,s){await this.call("events.publish",{topic:t,...e!==void 0?{params:e}:{},...s?{scope:s}:{}})}state={get:async(t,e)=>(await this.call("state.get",{...e?{scope:e}:{},...t?{keys:t}:{}})).values??{},set:async(t,e)=>{await this.call("state.set",{...e?{scope:e}:{},values:t})}};signalk={subscribe:async(t,e)=>{let s=t.map(r=>`sk.${r}`),n=await this.subscribe(s,(r,a)=>e(a)),i;try{i=(await this.call("signalk.subscribe",{paths:t})).subscriptionId}catch(r){throw await n(),r}return async()=>{await n(),await this.call("signalk.unsubscribe",{subscriptionId:i}).catch(()=>{})}},put:(t,e)=>this.call("signalk.put",{path:t,value:e})};route={list:async()=>(await this.call("route.list")).routes??[],create:async t=>await this.call("route.create",t),get:async t=>await this.call("route.get",{routeId:t}),replace:async(t,e)=>await this.call("route.replace",{routeId:t,points:e}),save:async(t,e)=>await this.call("route.save",{routeId:t,...e??{}}),show:async t=>await this.call("route.show",{ref:t}),hide:async t=>{await this.call("route.hide",{routeId:t})},delete:async t=>{await this.call("route.delete",{routeId:t})}};chart={list:async()=>(await this.call("chart.list")).charts??[],setVisibility:async(t,e)=>{await this.call("chart.setVisibility",{ids:t,visible:e})},setOpacity:async(t,e)=>{await this.call("chart.setOpacity",{ids:t,opacity:e})},setOrder:async t=>{await this.call("chart.setOrder",{order:t})},setTime:async(t,e)=>{await this.call("chart.setTime",{ids:t,time:e})}};nightMode={get:async()=>await this.call("nightMode.get"),set:async t=>{await this.call("nightMode.set",t)}};panels={list:async()=>(await this.call("ui.listPanels")).panels};overlays={list:async()=>(await this.call("overlay.list")).overlays,setVisibility:async(t,e)=>{await this.call("overlay.setVisibility",{ids:t,visible:e})},setOpacity:async(t,e)=>{await this.call("overlay.setOpacity",{ids:t,opacity:e})},frame:async t=>await this.call("overlay.frame",t,{transfer:[t.image]}),refresh:async t=>{await this.call("overlay.refresh",t)},setData:async t=>{await this.call("overlay.setData",t)},setLayer:async t=>{await this.call("overlay.setLayer",t)},setState:async(t,e)=>{await this.call("overlay.setState",{overlayId:t,values:e})}};windows={open:async t=>await this.call("ui.openWindow",t),update:async t=>await this.call("ui.updateWindow",t),focus:async t=>{await this.call("ui.focusWindow",t===void 0?{}:{windowId:t})},close:async t=>{await this.call("ui.closeWindow",t===void 0?{}:{windowId:t})},list:async()=>(await this.call("ui.listWindows")).windows};resourceGroup={apply:async t=>await this.call("resourceGroup.apply",{id:t})};map={getView:async()=>await this.call("map.getView"),center:async(t,e)=>{await this.call("map.center",{position:t,...typeof e=="number"?{zoom:e}:{}})},fitBounds:async t=>{await this.call("map.fitBounds",{bounds:t})}};close(){this.endpoint.close()}};function R(t={}){let e=t.port??h(globalThis.parent,{origin:"*"}),s=new p({port:e,callTimeoutMs:t.callTimeoutMs,onError:t.onError}),n=t.id?{id:t.id}:void 0;return new Promise((i,r)=>{let a=!1,d=s.onEvent([y],(I,T)=>{a||(a=!0,w(),i(new v(s,T)))}),b=setInterval(()=>s.notify(u,n),t.readyIntervalMs??250),O=setTimeout(()=>{a||(a=!0,w(),s.close(),r(new c("Timed out waiting for host handshake",{code:o.TIMEOUT,reason:"HANDSHAKE_TIMEOUT"})))},t.timeoutMs??1e4),w=()=>{d(),clearInterval(b),clearTimeout(O)};s.notify(u,n)})}export{R as connectExtension,h as windowPort};
|