@streamorchard/grapevine 0.6.0 → 0.7.1
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/PROTOCOL.md +1 -1
- package/README.md +19 -4
- package/package.json +1 -1
- package/scripts/check-shipped.js +5 -4
- package/src/index.js +53 -0
- package/types/index.d.ts +11 -0
package/PROTOCOL.md
CHANGED
|
@@ -32,7 +32,7 @@ Every app has a **slug** (`a-z`, `0-9` and `-`, up to 40 characters, starting wi
|
|
|
32
32
|
- A socket left by a crash answers nothing (`ECONNREFUSED`): its app deletes it and listens again when it starts. If something does answer, another copy of the app is running, and the new copy doesn't start its link.
|
|
33
33
|
- An app only listens, and only looks for others, while at least one of its Connected apps switches is on.
|
|
34
34
|
|
|
35
|
-
**Who connects.** Of two apps, the one whose slug sorts first (plain string order) connects; the other waits for it
|
|
35
|
+
**Who connects.** Of two apps, the one whose slug sorts first (plain string order) connects; the other waits for it, so a pair normally opens one connection. An app may still connect the other way to check the link on its user's request (a Test button). If they ever do have two, both keep the connection opened by the app whose slug sorts first and close the other with `link.bye { reason: "duplicate" }`. It's the slug, not the id, because an app's id can change (see [Renaming an app](#renaming-an-app)) and its socket's name doesn't.
|
|
36
36
|
|
|
37
37
|
## Messages
|
|
38
38
|
|
package/README.md
CHANGED
|
@@ -131,8 +131,15 @@ Editors suggest every event's fields and underline mistakes, in plain JavaScript
|
|
|
131
131
|
| `formerIds` | Ids the app had before a rename |
|
|
132
132
|
| `shares` | What the two apps share: `{ out, in }`, capabilities this app provides that it uses, and the other way round |
|
|
133
133
|
|
|
134
|
-
|
|
135
|
-
|
|
134
|
+
Three calls go with buttons on the card:
|
|
135
|
+
|
|
136
|
+
- **Test:** `await link.test(appId)`. Connected, it's a round trip, `{ ok: true, ms, version, text }`, with `text` like "Stream Multiview 0.4.0 answered in 3 ms". Otherwise it tries straight away, from this side too, and says why it couldn't: `{ ok: false, reason, text }`. `reason` is one of:
|
|
137
|
+
- `not-running`: "Stream Multiview isn't open"
|
|
138
|
+
- `switched-off`: its Connect switch is off
|
|
139
|
+
- `off`: this app's is
|
|
140
|
+
- `pair-again`: with where to press it
|
|
141
|
+
- `incompatible`: which app to update
|
|
142
|
+
- `no-answer`, `not-started`, `unknown`
|
|
136
143
|
- **Disconnect:** `link.disconnect(appId)`. Turn your own switch for that app off too.
|
|
137
144
|
- **Pair again:** `link.pairAgain(appId)`.
|
|
138
145
|
|
|
@@ -190,12 +197,20 @@ When it ships a release that uses Grapevine, record it (next section).
|
|
|
190
197
|
|
|
191
198
|
Once an app ships a capability version, Grapevine records it. From then on that version can't change in place, and other apps can say which release to update to.
|
|
192
199
|
|
|
193
|
-
|
|
200
|
+
Give the app a script for it, in its `package.json`:
|
|
201
|
+
|
|
202
|
+
```json
|
|
203
|
+
"scripts": { "grapevine:check": "grapevine-check" }
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Then, in the app's repo, at the start of every change:
|
|
194
207
|
|
|
195
208
|
```sh
|
|
196
|
-
|
|
209
|
+
npm run grapevine:check
|
|
197
210
|
```
|
|
198
211
|
|
|
212
|
+
Use the script, not `npx grapevine-check`: in a checkout where Grapevine isn't installed yet, `npx` would fetch and run whatever package on npm has that name. `npm run` only uses what's installed.
|
|
213
|
+
|
|
199
214
|
- **It reads** the `grapevine` field from each of the app's release tags (`v*`), and Grapevine's record from GitHub (the installed copy if it's offline, or with `--offline`).
|
|
200
215
|
- **It lists** any capability version a release shipped that isn't recorded yet, with the command to record it, e.g. `npm run released -- com.streamorchard.peardrops 0.1.6 drops.farming@1.0 players@1.0`.
|
|
201
216
|
- **That command runs in a Grapevine checkout** (its `app-shipped` skill), followed by a pull request and a patch release.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@streamorchard/grapevine",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"description": "Lets desktop apps on the same computer find each other and talk, privately: local sockets, JSON-RPC, pairing and versioned capabilities.",
|
|
5
5
|
"license": "GPL-3.0-only",
|
|
6
6
|
"author": "StreamOrchard",
|
package/scripts/check-shipped.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// Run in an app's repo (
|
|
2
|
+
// Run in an app's repo, through a script ("grapevine:check": "grapevine-check"; never npx, which
|
|
3
|
+
// would fetch a package of that name when Grapevine isn't installed): lists the capability versions the app's releases
|
|
3
4
|
// have shipped that Grapevine hasn't recorded yet, with the command that records them (Grapevine's
|
|
4
5
|
// app-shipped skill). Until they're recorded, those versions can still be changed in place, and
|
|
5
6
|
// other apps can't say which release to update to.
|
|
@@ -7,9 +8,9 @@
|
|
|
7
8
|
// The app says what it speaks in its package.json, so every release tag has it:
|
|
8
9
|
// "grapevine": { "app": "com.streamorchard.peardrops", "provides": { "drops.farming": "1.0" }, "uses": { "players": "1.0" } }
|
|
9
10
|
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
11
|
+
// npm run grapevine:check prints what needs recording (exit 0)
|
|
12
|
+
// npm run grapevine:check -- --strict exits 1 when something does
|
|
13
|
+
// npm run grapevine:check -- --offline uses the record in the installed package, not GitHub's
|
|
13
14
|
'use strict';
|
|
14
15
|
|
|
15
16
|
const fs = require('fs');
|
package/src/index.js
CHANGED
|
@@ -418,6 +418,59 @@ class Link extends EventEmitter {
|
|
|
418
418
|
this.changed();
|
|
419
419
|
}
|
|
420
420
|
|
|
421
|
+
/**
|
|
422
|
+
* Test on a card: checks the link to that app now, and says how it went in words. Connected, a
|
|
423
|
+
* round trip and back; otherwise it tries straight away (from this side too, whichever app
|
|
424
|
+
* normally connects) and says why it couldn't.
|
|
425
|
+
* @returns {Promise<{ok: true, ms: number, version: string, text: string} | {ok: false, reason: string, text: string}>}
|
|
426
|
+
* reason: 'unknown' | 'off' | 'not-started' | 'not-running' | 'switched-off' | 'pair-again' | 'incompatible' | 'no-answer'
|
|
427
|
+
*/
|
|
428
|
+
async test(appId, { timeout = 4000 } = {}) {
|
|
429
|
+
const id = this.currentId(appId);
|
|
430
|
+
const app = this.knownApps.find((a) => a.id === id);
|
|
431
|
+
if (!app) return { ok: false, reason: 'unknown', text: `${appId} isn't an app Grapevine knows` };
|
|
432
|
+
if (!this.allowedAny(this.idsOf(id))) return { ok: false, reason: 'off', text: `Switched off here: switch on Connect with ${app.name} first` };
|
|
433
|
+
if (!this.running) return { ok: false, reason: 'not-started', text: `${this.self.name} isn't listening for other apps yet` };
|
|
434
|
+
if (!this.conns.has(id)) {
|
|
435
|
+
if (!paths.listSlugs(this.place).includes(app.slug)) return { ok: false, reason: 'not-running', text: `${app.name} isn't open` };
|
|
436
|
+
this.retry.delete(id);
|
|
437
|
+
this.why.delete(id);
|
|
438
|
+
const trying = () => [...this.opening].some((c) => (c.expectedId ?? c.peerId) === id);
|
|
439
|
+
if (!trying()) this.connect(app);
|
|
440
|
+
await new Promise((resolve) => {
|
|
441
|
+
const done = () => {
|
|
442
|
+
clearTimeout(timer);
|
|
443
|
+
this.off('change', check);
|
|
444
|
+
resolve();
|
|
445
|
+
};
|
|
446
|
+
const check = () => (this.conns.has(id) || this.why.has(id) || !trying()) && done();
|
|
447
|
+
const timer = setTimeout(done, timeout);
|
|
448
|
+
this.on('change', check);
|
|
449
|
+
check();
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
const conn = this.conns.get(id);
|
|
453
|
+
if (!conn) {
|
|
454
|
+
const why = this.why.get(id);
|
|
455
|
+
if (why?.status === 'pair-again') {
|
|
456
|
+
return { ok: false, reason: 'pair-again', text: `The pairing doesn't match: press Pair again ${why.keyHeldBy === 'them' ? `in ${app.name}` : 'here'}` };
|
|
457
|
+
}
|
|
458
|
+
if (why?.status === 'incompatible') {
|
|
459
|
+
return { ok: false, reason: 'incompatible', text: incompatible(this.self, why.theirApp ?? app, hs.PROTOCOL, why.theirProtocol ?? hs.PROTOCOL).text };
|
|
460
|
+
}
|
|
461
|
+
if (why?.reason === 'switched-off') return { ok: false, reason: 'switched-off', text: `${app.name} is open, but its Connect switch is off` };
|
|
462
|
+
return { ok: false, reason: 'no-answer', text: `${app.name} is open, but didn't answer` };
|
|
463
|
+
}
|
|
464
|
+
const started = performance.now();
|
|
465
|
+
try {
|
|
466
|
+
await conn.rpc.request('link.ping', {}, { timeout });
|
|
467
|
+
} catch {
|
|
468
|
+
return { ok: false, reason: 'no-answer', text: `${app.name} didn't answer` };
|
|
469
|
+
}
|
|
470
|
+
const ms = Math.max(1, Math.round(performance.now() - started));
|
|
471
|
+
return { ok: true, ms, version: conn.peer.version, text: `${app.name} ${conn.peer.version} answered in ${ms} ms` };
|
|
472
|
+
}
|
|
473
|
+
|
|
421
474
|
isConnected(appId) {
|
|
422
475
|
return this.conns.has(this.currentId(appId));
|
|
423
476
|
}
|
package/types/index.d.ts
CHANGED
|
@@ -128,6 +128,15 @@ export interface Peer {
|
|
|
128
128
|
keysEncrypted: boolean;
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
+
export type TestResult =
|
|
132
|
+
| { ok: true; ms: number; version: string; text: string }
|
|
133
|
+
| {
|
|
134
|
+
ok: false;
|
|
135
|
+
reason: 'unknown' | 'off' | 'not-started' | 'not-running' | 'switched-off' | 'pair-again' | 'incompatible' | 'no-answer';
|
|
136
|
+
/** In words, for the card: "Stream Multiview isn't open" */
|
|
137
|
+
text: string;
|
|
138
|
+
};
|
|
139
|
+
|
|
131
140
|
export interface CallContext {
|
|
132
141
|
appId: string;
|
|
133
142
|
app: AppInfo;
|
|
@@ -156,6 +165,8 @@ export declare class Link extends EventEmitter {
|
|
|
156
165
|
/** Pair again on a card: forgets this app's old key. */
|
|
157
166
|
pairAgain(appId: string): void;
|
|
158
167
|
isConnected(appId: string): boolean;
|
|
168
|
+
/** Test on a card: checks the link now, and says how it went. Tries straight away when not connected. */
|
|
169
|
+
test(appId: string, opts?: { timeout?: number }): Promise<TestResult>;
|
|
159
170
|
/** Its current id, given any id it has had: for moving your own settings across after another app is renamed. */
|
|
160
171
|
currentId(appId: string): string;
|
|
161
172
|
/** Every known app, for the Connected apps page. */
|