@graffy/client 0.19.0 → 0.19.1-alpha.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/Socket.js ADDED
@@ -0,0 +1,149 @@
1
+ /*
2
+ This class implements a self-healing wrapper around a WebSocket, with
3
+ a higher level concept of "requests". Requests are retried when a connection
4
+ reconnects.
5
+
6
+ This implements reconnection with exponential backoff that is designed to
7
+ work reasonably well on platforms that may "forget" long-running timers
8
+ such as React Native.
9
+
10
+ The isAlive exported function may be called in situations such as the app
11
+ being restored, where timers may have been forgotten.
12
+ */
13
+ import { makeId } from '@graffy/common';
14
+ import debug from 'debug';
15
+ const log = debug('graffy:client:socket');
16
+ log.log = console.log.bind(console);
17
+ const MIN_DELAY = 1000;
18
+ const MAX_DELAY = 300000;
19
+ const DELAY_GROWTH = 1.5;
20
+ const INTERVAL = 2000;
21
+ const PING_TIMEOUT = 40000; // Make this greater than server interval.
22
+ const RESET_TIMEOUT = 10000;
23
+ export default function Socket(url, { onUnhandled = undefined, onStatusChange = undefined } = {}) {
24
+ const handlers = {};
25
+ const buffer = [];
26
+ let isOpen = false;
27
+ let isConnecting = false;
28
+ let socket;
29
+ let lastAlive;
30
+ let lastAttempt;
31
+ let attempts = 0;
32
+ let connectTimer;
33
+ let aliveTimer;
34
+ function start(params, callback) {
35
+ const id = makeId();
36
+ const request = [id, ...params];
37
+ handlers[id] = { request, callback };
38
+ if (isAlive())
39
+ send(request);
40
+ return id;
41
+ }
42
+ function stop(id, params) {
43
+ delete handlers[id];
44
+ if (params)
45
+ send([id, ...params]);
46
+ }
47
+ function connect() {
48
+ log('Trying to connect to', url);
49
+ isOpen = false;
50
+ isConnecting = true;
51
+ lastAttempt = Date.now();
52
+ attempts++;
53
+ socket = new globalThis.WebSocket(url);
54
+ socket.onmessage = received;
55
+ socket.onerror = closed;
56
+ socket.onclose = closed;
57
+ socket.onopen = opened;
58
+ }
59
+ function received(event) {
60
+ const [id, ...data] = JSON.parse(event.data);
61
+ setAlive();
62
+ if (id === ':ping') {
63
+ send([':pong']);
64
+ }
65
+ else if (handlers[id]) {
66
+ handlers[id].callback(...data);
67
+ }
68
+ else {
69
+ // We received an unexpected push.
70
+ onUnhandled?.(id, ...data);
71
+ }
72
+ }
73
+ function closed(_event) {
74
+ log('Closed');
75
+ if (isOpen && onStatusChange)
76
+ onStatusChange(false);
77
+ const wasOpen = isOpen;
78
+ isOpen = false;
79
+ isConnecting = false;
80
+ lastAttempt = Date.now();
81
+ if (wasOpen && !attempts) {
82
+ // Quick reconnect path if we previously had a stable connection.
83
+ connect();
84
+ return;
85
+ }
86
+ maybeConnect();
87
+ }
88
+ function maybeConnect() {
89
+ const connDelay = lastAttempt +
90
+ Math.min(MAX_DELAY, MIN_DELAY * DELAY_GROWTH ** attempts) -
91
+ Date.now();
92
+ log('Will reconnect in', connDelay, 'ms');
93
+ if (connDelay <= 0) {
94
+ connect();
95
+ return;
96
+ }
97
+ clearTimeout(connectTimer);
98
+ connectTimer = setTimeout(connect, connDelay);
99
+ }
100
+ function opened() {
101
+ log('Connected', buffer.length, Object.keys(handlers).length);
102
+ isOpen = true;
103
+ isConnecting = false;
104
+ lastAttempt = Date.now();
105
+ setAlive();
106
+ if (onStatusChange)
107
+ onStatusChange(true);
108
+ for (const id in handlers)
109
+ send(handlers[id].request);
110
+ while (buffer.length)
111
+ send(buffer.shift());
112
+ }
113
+ function setAlive() {
114
+ lastAlive = Date.now();
115
+ log('Set alive', lastAlive - lastAttempt);
116
+ if (lastAlive - lastAttempt > RESET_TIMEOUT)
117
+ attempts = 0;
118
+ }
119
+ function isAlive() {
120
+ log('Liveness check', isOpen ? 'open' : 'closed', Date.now() - lastAlive);
121
+ clearTimeout(aliveTimer);
122
+ aliveTimer = setTimeout(isAlive, INTERVAL);
123
+ if (!isOpen) {
124
+ if (!isConnecting)
125
+ maybeConnect();
126
+ return false;
127
+ }
128
+ if (Date.now() - lastAlive < PING_TIMEOUT)
129
+ return true;
130
+ log('Ping timeout, closing', lastAlive);
131
+ socket.close();
132
+ return false;
133
+ }
134
+ function send(req) {
135
+ if (isAlive()) {
136
+ socket.send(JSON.stringify(req));
137
+ }
138
+ else {
139
+ buffer.push(req);
140
+ }
141
+ }
142
+ connect();
143
+ aliveTimer = setTimeout(isAlive, INTERVAL);
144
+ return {
145
+ start,
146
+ stop,
147
+ isAlive,
148
+ };
149
+ }
@@ -1,4 +1,3 @@
1
- export default httpClient;
2
1
  /**
3
2
  *
4
3
  * @param {string} baseUrl
@@ -9,8 +8,9 @@ export default httpClient;
9
8
  * } | undefined} options
10
9
  * @returns {(store: any) => void}
11
10
  */
12
- declare function httpClient(baseUrl: string, { getOptions, watch, connInfoPath, }?: {
13
- getOptions?: (op: string, options: any) => Promise<void>;
14
- watch?: "sse" | "none" | "hang";
11
+ declare const httpClient: (baseUrl: any, { getOptions, watch, connInfoPath, }?: {
12
+ getOptions?: () => Promise<void>;
13
+ watch?: string;
15
14
  connInfoPath?: string;
16
- } | undefined): (store: any) => void;
15
+ }) => (store: any) => void;
16
+ export default httpClient;
package/httpClient.js ADDED
@@ -0,0 +1,126 @@
1
+ import { add, pack, unpack } from '@graffy/common';
2
+ import { makeStream } from '@graffy/stream';
3
+ function getOptionsParam(options) {
4
+ if (!options)
5
+ return '';
6
+ return encodeURIComponent(JSON.stringify(options));
7
+ }
8
+ const aggregateQueries = {};
9
+ class AggregateQuery {
10
+ constructor(url) {
11
+ this.combinedQuery = [];
12
+ this.readers = [];
13
+ this.timer = null;
14
+ this.url = url;
15
+ }
16
+ add(query) {
17
+ add(this.combinedQuery, query);
18
+ if (this.timer)
19
+ clearTimeout(this.timer);
20
+ this.timer = setTimeout(() => this.doFetch(), 0);
21
+ return new Promise((resolve, reject) => {
22
+ this.readers.push({ query, resolve, reject });
23
+ });
24
+ }
25
+ async doFetch() {
26
+ delete aggregateQueries[this.url];
27
+ const response = await fetch(this.url, {
28
+ method: 'POST',
29
+ headers: { 'Content-Type': 'application/json' },
30
+ body: JSON.stringify(pack(this.combinedQuery)),
31
+ });
32
+ if (response.status !== 200) {
33
+ const message = await response.text();
34
+ const err = new Error(message);
35
+ for (const reader of this.readers) {
36
+ reader.reject(err);
37
+ }
38
+ return;
39
+ }
40
+ try {
41
+ const data = unpack(JSON.parse(await response.text()));
42
+ for (const reader of this.readers)
43
+ reader.resolve(data);
44
+ }
45
+ catch (e) {
46
+ for (const reader of this.readers)
47
+ reader.reject(e);
48
+ }
49
+ }
50
+ }
51
+ function makeQuery(url, query) {
52
+ if (!aggregateQueries[url])
53
+ aggregateQueries[url] = new AggregateQuery(url);
54
+ return aggregateQueries[url].add(query);
55
+ }
56
+ /**
57
+ *
58
+ * @param {string} baseUrl
59
+ * @param {{
60
+ * getOptions?: (op: string, options: any) => Promise<void>,
61
+ * watch?: 'sse' | 'none' | 'hang',
62
+ * connInfoPath?: string,
63
+ * } | undefined} options
64
+ * @returns {(store: any) => void}
65
+ */
66
+ const httpClient = (baseUrl, { getOptions = async () => { }, watch = 'sse', connInfoPath = 'connection', } = {}) => (store) => {
67
+ store.onWrite(connInfoPath, ({ url }) => {
68
+ baseUrl = url;
69
+ return { url };
70
+ });
71
+ store.onRead(connInfoPath, () => ({ url: baseUrl }));
72
+ store.on('read', async (query, options) => {
73
+ if (!fetch)
74
+ throw Error('client.fetch.unavailable');
75
+ const optionsParam = getOptionsParam(await getOptions('read', options));
76
+ const url = `${baseUrl}?opts=${optionsParam}&op=read`;
77
+ return makeQuery(url, query);
78
+ });
79
+ store.on('watch', async function* (query, options) {
80
+ if (watch === 'none')
81
+ throw Error('client.no_watch');
82
+ if (watch === 'hang') {
83
+ yield* makeStream((push) => {
84
+ push(undefined);
85
+ });
86
+ return;
87
+ }
88
+ if (!EventSource)
89
+ throw Error('client.sse.unavailable');
90
+ const optionsParam = getOptionsParam(await getOptions('watch', options));
91
+ const url = `${baseUrl}?q=${encodeURIComponent(JSON.stringify(pack(query)))}&opts=${optionsParam}`;
92
+ const source = new EventSource(url);
93
+ yield* makeStream((push, end) => {
94
+ source.onmessage = ({ data }) => {
95
+ push(unpack(JSON.parse(data)));
96
+ };
97
+ source.onerror = (e) => {
98
+ end(Error(`client.sse.transport: ${e}`));
99
+ };
100
+ source.addEventListener('graffyerror', (e) => {
101
+ end(Error(`server.${e.data}`));
102
+ });
103
+ return () => {
104
+ source.close();
105
+ };
106
+ });
107
+ });
108
+ store.on('write', async (change, options) => {
109
+ if (!fetch)
110
+ throw Error('client.fetch.unavailable');
111
+ const optionsParam = getOptionsParam(await getOptions('write', options));
112
+ const url = `${baseUrl}?opts=${optionsParam}&op=write`;
113
+ return fetch(url, {
114
+ method: 'POST',
115
+ headers: { 'Content-Type': 'application/json' },
116
+ body: JSON.stringify(pack(change)),
117
+ }).then(async (res) => {
118
+ if (res.status === 200)
119
+ return unpack(JSON.parse(await res.text()));
120
+ return res.text().then((message) => {
121
+ throw Error(`server.${message}`);
122
+ });
123
+ });
124
+ });
125
+ };
126
+ export default httpClient;
package/index.js ADDED
@@ -0,0 +1,9 @@
1
+ import httpClient from "./httpClient.js";
2
+ import wsClient from "./wsClient.js";
3
+ const WSRE = /^wss?:\/\//;
4
+ export default function GraffyClient(baseUrl, options) {
5
+ if (WSRE.test(baseUrl)) {
6
+ return wsClient(baseUrl, options);
7
+ }
8
+ return httpClient(baseUrl, options);
9
+ }
package/package.json CHANGED
@@ -2,22 +2,27 @@
2
2
  "name": "@graffy/client",
3
3
  "description": "Graffy client library for the browser, usin the `fetch()` or `WebSocket` APIs. This module is intended to be used with a Node.js server running Graffy Server.",
4
4
  "author": "aravind (https://github.com/aravindet)",
5
- "version": "0.19.0",
6
- "main": "./index.cjs",
5
+ "version": "0.19.1-alpha.1",
6
+ "main": "./cjs/index.js",
7
7
  "exports": {
8
- "import": "./index.mjs",
9
- "require": "./index.cjs"
8
+ ".": {
9
+ "import": "./index.js",
10
+ "types": "./index.d.ts"
11
+ },
12
+ "./*": {
13
+ "import": "./*.js",
14
+ "types": "./*.d.ts"
15
+ }
10
16
  },
11
- "module": "./index.mjs",
12
- "types": "./types/index.d.ts",
17
+ "types": "./index.d.ts",
13
18
  "repository": {
14
19
  "type": "git",
15
20
  "url": "git+https://github.com/usegraffy/graffy.git"
16
21
  },
17
22
  "license": "Apache-2.0",
18
23
  "dependencies": {
19
- "@graffy/common": "0.19.0",
20
- "@graffy/stream": "0.19.0",
24
+ "@graffy/common": "0.19.1-alpha.1",
25
+ "@graffy/stream": "0.19.1-alpha.1",
21
26
  "debug": "^4.4.3"
22
27
  }
23
28
  }
@@ -1,6 +1,6 @@
1
- export default wsClient;
2
- declare function wsClient(url: any, { getOptions, watch, connInfoPath, }?: {
1
+ declare const wsClient: (url: any, { getOptions, watch, connInfoPath, }?: {
3
2
  getOptions?: (..._: any[]) => boolean;
4
3
  watch?: any;
5
4
  connInfoPath?: string;
6
- }): (store: any) => void;
5
+ }) => (store: any) => void;
6
+ export default wsClient;
package/wsClient.js ADDED
@@ -0,0 +1,52 @@
1
+ import { makeWatcher, pack, unpack } from '@graffy/common';
2
+ import { makeStream } from '@graffy/stream';
3
+ import Socket from "./Socket.js";
4
+ const wsClient = (url, { getOptions = (..._) => false, watch = undefined, connInfoPath = 'connection', } = {}) => (store) => {
5
+ if (!WebSocket)
6
+ throw Error('client.websocket.unavailable');
7
+ const socket = Socket(url, { onUnhandled, onStatusChange });
8
+ let status = false;
9
+ const statusWatcher = makeWatcher();
10
+ function onUnhandled(id) {
11
+ socket.stop(id, ['unwatch']);
12
+ }
13
+ function onStatusChange(newStatus) {
14
+ status = newStatus;
15
+ statusWatcher.write({ status });
16
+ }
17
+ function once(op, payload, options) {
18
+ return new Promise((resolve, reject) => {
19
+ const id = socket.start([op, pack(payload), getOptions(op, options) || {}], (error, result) => {
20
+ socket.stop(id);
21
+ error ? reject(Error(`server.${error}`)) : resolve(unpack(result));
22
+ });
23
+ });
24
+ }
25
+ store.onWrite(connInfoPath, () => {
26
+ status = socket.isAlive();
27
+ return { status };
28
+ });
29
+ store.onRead(connInfoPath, () => ({ status }));
30
+ store.onWatch(connInfoPath, () => statusWatcher.watch({ status }));
31
+ store.on('read', (query, options) => once('read', query, options));
32
+ store.on('write', (change, options) => once('write', change, options));
33
+ store.on('watch', (query, options) => {
34
+ if (watch === 'none')
35
+ throw Error('client.no_watch');
36
+ const op = 'watch';
37
+ return makeStream((push, end) => {
38
+ const id = socket.start([op, pack(query), getOptions(op, options) || {}], (error, result) => {
39
+ if (error) {
40
+ socket.stop(id);
41
+ end(Error(`server.${error}`));
42
+ return;
43
+ }
44
+ push(unpack(result));
45
+ });
46
+ return () => {
47
+ socket.stop(id, ['unwatch']);
48
+ };
49
+ });
50
+ });
51
+ };
52
+ export default wsClient;
package/index.cjs DELETED
@@ -1,296 +0,0 @@
1
- "use strict";
2
- const common = require("@graffy/common");
3
- const stream = require("@graffy/stream");
4
- const debug = require("debug");
5
- function getOptionsParam(options) {
6
- if (!options) return "";
7
- return encodeURIComponent(JSON.stringify(options));
8
- }
9
- const aggregateQueries = {};
10
- class AggregateQuery {
11
- combinedQuery = [];
12
- readers = [];
13
- timer = null;
14
- constructor(url) {
15
- this.url = url;
16
- }
17
- add(query) {
18
- common.add(this.combinedQuery, query);
19
- if (this.timer) clearTimeout(this.timer);
20
- this.timer = setTimeout(() => this.doFetch(), 0);
21
- return new Promise((resolve, reject) => {
22
- this.readers.push({ query, resolve, reject });
23
- });
24
- }
25
- async doFetch() {
26
- delete aggregateQueries[this.url];
27
- const response = await fetch(this.url, {
28
- method: "POST",
29
- headers: { "Content-Type": "application/json" },
30
- body: JSON.stringify(common.pack(this.combinedQuery))
31
- });
32
- if (response.status !== 200) {
33
- const message = await response.text();
34
- const err = new Error(message);
35
- for (const reader of this.readers) {
36
- reader.reject(err);
37
- }
38
- return;
39
- }
40
- try {
41
- const data = common.unpack(JSON.parse(await response.text()));
42
- for (const reader of this.readers) reader.resolve(data);
43
- } catch (e) {
44
- for (const reader of this.readers) reader.reject(e);
45
- }
46
- }
47
- }
48
- function makeQuery(url, query) {
49
- if (!aggregateQueries[url]) aggregateQueries[url] = new AggregateQuery(url);
50
- return aggregateQueries[url].add(query);
51
- }
52
- const httpClient = (baseUrl, {
53
- getOptions = async () => {
54
- },
55
- watch = "sse",
56
- connInfoPath = "connection"
57
- } = {}) => (store) => {
58
- store.onWrite(connInfoPath, ({ url }) => {
59
- baseUrl = url;
60
- return { url };
61
- });
62
- store.onRead(connInfoPath, () => ({ url: baseUrl }));
63
- store.on("read", async (query, options) => {
64
- if (!fetch) throw Error("client.fetch.unavailable");
65
- const optionsParam = getOptionsParam(await getOptions("read", options));
66
- const url = `${baseUrl}?opts=${optionsParam}&op=read`;
67
- return makeQuery(url, query);
68
- });
69
- store.on("watch", async function* (query, options) {
70
- if (watch === "none") throw Error("client.no_watch");
71
- if (watch === "hang") {
72
- yield* stream.makeStream((push) => {
73
- push(void 0);
74
- });
75
- return;
76
- }
77
- if (!EventSource) throw Error("client.sse.unavailable");
78
- const optionsParam = getOptionsParam(await getOptions("watch", options));
79
- const url = `${baseUrl}?q=${encodeURIComponent(
80
- JSON.stringify(common.pack(query))
81
- )}&opts=${optionsParam}`;
82
- const source = new EventSource(url);
83
- yield* stream.makeStream((push, end) => {
84
- source.onmessage = ({ data }) => {
85
- push(common.unpack(JSON.parse(data)));
86
- };
87
- source.onerror = (e) => {
88
- end(Error(`client.sse.transport: ${e}`));
89
- };
90
- source.addEventListener("graffyerror", (e) => {
91
- end(Error(`server.${e.data}`));
92
- });
93
- return () => {
94
- source.close();
95
- };
96
- });
97
- });
98
- store.on("write", async (change, options) => {
99
- if (!fetch) throw Error("client.fetch.unavailable");
100
- const optionsParam = getOptionsParam(await getOptions("write", options));
101
- const url = `${baseUrl}?opts=${optionsParam}&op=write`;
102
- return fetch(url, {
103
- method: "POST",
104
- headers: { "Content-Type": "application/json" },
105
- body: JSON.stringify(common.pack(change))
106
- }).then(async (res) => {
107
- if (res.status === 200) return common.unpack(JSON.parse(await res.text()));
108
- return res.text().then((message) => {
109
- throw Error(`server.${message}`);
110
- });
111
- });
112
- });
113
- };
114
- const log = debug("graffy:client:socket");
115
- log.log = console.log.bind(console);
116
- const MIN_DELAY = 1e3;
117
- const MAX_DELAY = 3e5;
118
- const DELAY_GROWTH = 1.5;
119
- const INTERVAL = 2e3;
120
- const PING_TIMEOUT = 4e4;
121
- const RESET_TIMEOUT = 1e4;
122
- function Socket(url, { onUnhandled = void 0, onStatusChange = void 0 } = {}) {
123
- const handlers = {};
124
- const buffer = [];
125
- let isOpen = false;
126
- let isConnecting = false;
127
- let socket;
128
- let lastAlive;
129
- let lastAttempt;
130
- let attempts = 0;
131
- let connectTimer;
132
- let aliveTimer;
133
- function start(params, callback) {
134
- const id = common.makeId();
135
- const request = [id, ...params];
136
- handlers[id] = { request, callback };
137
- if (isAlive()) send(request);
138
- return id;
139
- }
140
- function stop(id, params) {
141
- delete handlers[id];
142
- if (params) send([id, ...params]);
143
- }
144
- function connect() {
145
- log("Trying to connect to", url);
146
- isOpen = false;
147
- isConnecting = true;
148
- lastAttempt = Date.now();
149
- attempts++;
150
- socket = new globalThis.WebSocket(url);
151
- socket.onmessage = received;
152
- socket.onerror = closed;
153
- socket.onclose = closed;
154
- socket.onopen = opened;
155
- }
156
- function received(event) {
157
- const [id, ...data] = JSON.parse(event.data);
158
- setAlive();
159
- if (id === ":ping") {
160
- send([":pong"]);
161
- } else if (handlers[id]) {
162
- handlers[id].callback(...data);
163
- } else {
164
- onUnhandled?.(id, ...data);
165
- }
166
- }
167
- function closed(_event) {
168
- log("Closed");
169
- if (isOpen && onStatusChange) onStatusChange(false);
170
- const wasOpen = isOpen;
171
- isOpen = false;
172
- isConnecting = false;
173
- lastAttempt = Date.now();
174
- if (wasOpen && !attempts) {
175
- connect();
176
- return;
177
- }
178
- maybeConnect();
179
- }
180
- function maybeConnect() {
181
- const connDelay = lastAttempt + Math.min(MAX_DELAY, MIN_DELAY * DELAY_GROWTH ** attempts) - Date.now();
182
- log("Will reconnect in", connDelay, "ms");
183
- if (connDelay <= 0) {
184
- connect();
185
- return;
186
- }
187
- clearTimeout(connectTimer);
188
- connectTimer = setTimeout(connect, connDelay);
189
- }
190
- function opened() {
191
- log("Connected", buffer.length, Object.keys(handlers).length);
192
- isOpen = true;
193
- isConnecting = false;
194
- lastAttempt = Date.now();
195
- setAlive();
196
- if (onStatusChange) onStatusChange(true);
197
- for (const id in handlers) send(handlers[id].request);
198
- while (buffer.length) send(buffer.shift());
199
- }
200
- function setAlive() {
201
- lastAlive = Date.now();
202
- log("Set alive", lastAlive - lastAttempt);
203
- if (lastAlive - lastAttempt > RESET_TIMEOUT) attempts = 0;
204
- }
205
- function isAlive() {
206
- log("Liveness check", isOpen ? "open" : "closed", Date.now() - lastAlive);
207
- clearTimeout(aliveTimer);
208
- aliveTimer = setTimeout(isAlive, INTERVAL);
209
- if (!isOpen) {
210
- if (!isConnecting) maybeConnect();
211
- return false;
212
- }
213
- if (Date.now() - lastAlive < PING_TIMEOUT) return true;
214
- log("Ping timeout, closing", lastAlive);
215
- socket.close();
216
- return false;
217
- }
218
- function send(req) {
219
- if (isAlive()) {
220
- socket.send(JSON.stringify(req));
221
- } else {
222
- buffer.push(req);
223
- }
224
- }
225
- connect();
226
- aliveTimer = setTimeout(isAlive, INTERVAL);
227
- return {
228
- start,
229
- stop,
230
- isAlive
231
- };
232
- }
233
- const wsClient = (url, {
234
- getOptions = (..._) => false,
235
- watch = void 0,
236
- connInfoPath = "connection"
237
- } = {}) => (store) => {
238
- if (!WebSocket) throw Error("client.websocket.unavailable");
239
- const socket = Socket(url, { onUnhandled, onStatusChange });
240
- let status = false;
241
- const statusWatcher = common.makeWatcher();
242
- function onUnhandled(id) {
243
- socket.stop(id, ["unwatch"]);
244
- }
245
- function onStatusChange(newStatus) {
246
- status = newStatus;
247
- statusWatcher.write({ status });
248
- }
249
- function once(op, payload, options) {
250
- return new Promise((resolve, reject) => {
251
- const id = socket.start(
252
- [op, common.pack(payload), getOptions(op, options) || {}],
253
- (error, result) => {
254
- socket.stop(id);
255
- error ? reject(Error(`server.${error}`)) : resolve(common.unpack(result));
256
- }
257
- );
258
- });
259
- }
260
- store.onWrite(connInfoPath, () => {
261
- status = socket.isAlive();
262
- return { status };
263
- });
264
- store.onRead(connInfoPath, () => ({ status }));
265
- store.onWatch(connInfoPath, () => statusWatcher.watch({ status }));
266
- store.on("read", (query, options) => once("read", query, options));
267
- store.on("write", (change, options) => once("write", change, options));
268
- store.on("watch", (query, options) => {
269
- if (watch === "none") throw Error("client.no_watch");
270
- const op = "watch";
271
- return stream.makeStream((push, end) => {
272
- const id = socket.start(
273
- [op, common.pack(query), getOptions(op, options) || {}],
274
- (error, result) => {
275
- if (error) {
276
- socket.stop(id);
277
- end(Error(`server.${error}`));
278
- return;
279
- }
280
- push(common.unpack(result));
281
- }
282
- );
283
- return () => {
284
- socket.stop(id, ["unwatch"]);
285
- };
286
- });
287
- });
288
- };
289
- const WSRE = /^wss?:\/\//;
290
- function GraffyClient(baseUrl, options) {
291
- if (WSRE.test(baseUrl)) {
292
- return wsClient(baseUrl, options);
293
- }
294
- return httpClient(baseUrl, options);
295
- }
296
- module.exports = GraffyClient;
package/index.mjs DELETED
@@ -1,297 +0,0 @@
1
- import { pack, unpack, add, makeId, makeWatcher } from "@graffy/common";
2
- import { makeStream } from "@graffy/stream";
3
- import debug from "debug";
4
- function getOptionsParam(options) {
5
- if (!options) return "";
6
- return encodeURIComponent(JSON.stringify(options));
7
- }
8
- const aggregateQueries = {};
9
- class AggregateQuery {
10
- combinedQuery = [];
11
- readers = [];
12
- timer = null;
13
- constructor(url) {
14
- this.url = url;
15
- }
16
- add(query) {
17
- add(this.combinedQuery, query);
18
- if (this.timer) clearTimeout(this.timer);
19
- this.timer = setTimeout(() => this.doFetch(), 0);
20
- return new Promise((resolve, reject) => {
21
- this.readers.push({ query, resolve, reject });
22
- });
23
- }
24
- async doFetch() {
25
- delete aggregateQueries[this.url];
26
- const response = await fetch(this.url, {
27
- method: "POST",
28
- headers: { "Content-Type": "application/json" },
29
- body: JSON.stringify(pack(this.combinedQuery))
30
- });
31
- if (response.status !== 200) {
32
- const message = await response.text();
33
- const err = new Error(message);
34
- for (const reader of this.readers) {
35
- reader.reject(err);
36
- }
37
- return;
38
- }
39
- try {
40
- const data = unpack(JSON.parse(await response.text()));
41
- for (const reader of this.readers) reader.resolve(data);
42
- } catch (e) {
43
- for (const reader of this.readers) reader.reject(e);
44
- }
45
- }
46
- }
47
- function makeQuery(url, query) {
48
- if (!aggregateQueries[url]) aggregateQueries[url] = new AggregateQuery(url);
49
- return aggregateQueries[url].add(query);
50
- }
51
- const httpClient = (baseUrl, {
52
- getOptions = async () => {
53
- },
54
- watch = "sse",
55
- connInfoPath = "connection"
56
- } = {}) => (store) => {
57
- store.onWrite(connInfoPath, ({ url }) => {
58
- baseUrl = url;
59
- return { url };
60
- });
61
- store.onRead(connInfoPath, () => ({ url: baseUrl }));
62
- store.on("read", async (query, options) => {
63
- if (!fetch) throw Error("client.fetch.unavailable");
64
- const optionsParam = getOptionsParam(await getOptions("read", options));
65
- const url = `${baseUrl}?opts=${optionsParam}&op=read`;
66
- return makeQuery(url, query);
67
- });
68
- store.on("watch", async function* (query, options) {
69
- if (watch === "none") throw Error("client.no_watch");
70
- if (watch === "hang") {
71
- yield* makeStream((push) => {
72
- push(void 0);
73
- });
74
- return;
75
- }
76
- if (!EventSource) throw Error("client.sse.unavailable");
77
- const optionsParam = getOptionsParam(await getOptions("watch", options));
78
- const url = `${baseUrl}?q=${encodeURIComponent(
79
- JSON.stringify(pack(query))
80
- )}&opts=${optionsParam}`;
81
- const source = new EventSource(url);
82
- yield* makeStream((push, end) => {
83
- source.onmessage = ({ data }) => {
84
- push(unpack(JSON.parse(data)));
85
- };
86
- source.onerror = (e) => {
87
- end(Error(`client.sse.transport: ${e}`));
88
- };
89
- source.addEventListener("graffyerror", (e) => {
90
- end(Error(`server.${e.data}`));
91
- });
92
- return () => {
93
- source.close();
94
- };
95
- });
96
- });
97
- store.on("write", async (change, options) => {
98
- if (!fetch) throw Error("client.fetch.unavailable");
99
- const optionsParam = getOptionsParam(await getOptions("write", options));
100
- const url = `${baseUrl}?opts=${optionsParam}&op=write`;
101
- return fetch(url, {
102
- method: "POST",
103
- headers: { "Content-Type": "application/json" },
104
- body: JSON.stringify(pack(change))
105
- }).then(async (res) => {
106
- if (res.status === 200) return unpack(JSON.parse(await res.text()));
107
- return res.text().then((message) => {
108
- throw Error(`server.${message}`);
109
- });
110
- });
111
- });
112
- };
113
- const log = debug("graffy:client:socket");
114
- log.log = console.log.bind(console);
115
- const MIN_DELAY = 1e3;
116
- const MAX_DELAY = 3e5;
117
- const DELAY_GROWTH = 1.5;
118
- const INTERVAL = 2e3;
119
- const PING_TIMEOUT = 4e4;
120
- const RESET_TIMEOUT = 1e4;
121
- function Socket(url, { onUnhandled = void 0, onStatusChange = void 0 } = {}) {
122
- const handlers = {};
123
- const buffer = [];
124
- let isOpen = false;
125
- let isConnecting = false;
126
- let socket;
127
- let lastAlive;
128
- let lastAttempt;
129
- let attempts = 0;
130
- let connectTimer;
131
- let aliveTimer;
132
- function start(params, callback) {
133
- const id = makeId();
134
- const request = [id, ...params];
135
- handlers[id] = { request, callback };
136
- if (isAlive()) send(request);
137
- return id;
138
- }
139
- function stop(id, params) {
140
- delete handlers[id];
141
- if (params) send([id, ...params]);
142
- }
143
- function connect() {
144
- log("Trying to connect to", url);
145
- isOpen = false;
146
- isConnecting = true;
147
- lastAttempt = Date.now();
148
- attempts++;
149
- socket = new globalThis.WebSocket(url);
150
- socket.onmessage = received;
151
- socket.onerror = closed;
152
- socket.onclose = closed;
153
- socket.onopen = opened;
154
- }
155
- function received(event) {
156
- const [id, ...data] = JSON.parse(event.data);
157
- setAlive();
158
- if (id === ":ping") {
159
- send([":pong"]);
160
- } else if (handlers[id]) {
161
- handlers[id].callback(...data);
162
- } else {
163
- onUnhandled?.(id, ...data);
164
- }
165
- }
166
- function closed(_event) {
167
- log("Closed");
168
- if (isOpen && onStatusChange) onStatusChange(false);
169
- const wasOpen = isOpen;
170
- isOpen = false;
171
- isConnecting = false;
172
- lastAttempt = Date.now();
173
- if (wasOpen && !attempts) {
174
- connect();
175
- return;
176
- }
177
- maybeConnect();
178
- }
179
- function maybeConnect() {
180
- const connDelay = lastAttempt + Math.min(MAX_DELAY, MIN_DELAY * DELAY_GROWTH ** attempts) - Date.now();
181
- log("Will reconnect in", connDelay, "ms");
182
- if (connDelay <= 0) {
183
- connect();
184
- return;
185
- }
186
- clearTimeout(connectTimer);
187
- connectTimer = setTimeout(connect, connDelay);
188
- }
189
- function opened() {
190
- log("Connected", buffer.length, Object.keys(handlers).length);
191
- isOpen = true;
192
- isConnecting = false;
193
- lastAttempt = Date.now();
194
- setAlive();
195
- if (onStatusChange) onStatusChange(true);
196
- for (const id in handlers) send(handlers[id].request);
197
- while (buffer.length) send(buffer.shift());
198
- }
199
- function setAlive() {
200
- lastAlive = Date.now();
201
- log("Set alive", lastAlive - lastAttempt);
202
- if (lastAlive - lastAttempt > RESET_TIMEOUT) attempts = 0;
203
- }
204
- function isAlive() {
205
- log("Liveness check", isOpen ? "open" : "closed", Date.now() - lastAlive);
206
- clearTimeout(aliveTimer);
207
- aliveTimer = setTimeout(isAlive, INTERVAL);
208
- if (!isOpen) {
209
- if (!isConnecting) maybeConnect();
210
- return false;
211
- }
212
- if (Date.now() - lastAlive < PING_TIMEOUT) return true;
213
- log("Ping timeout, closing", lastAlive);
214
- socket.close();
215
- return false;
216
- }
217
- function send(req) {
218
- if (isAlive()) {
219
- socket.send(JSON.stringify(req));
220
- } else {
221
- buffer.push(req);
222
- }
223
- }
224
- connect();
225
- aliveTimer = setTimeout(isAlive, INTERVAL);
226
- return {
227
- start,
228
- stop,
229
- isAlive
230
- };
231
- }
232
- const wsClient = (url, {
233
- getOptions = (..._) => false,
234
- watch = void 0,
235
- connInfoPath = "connection"
236
- } = {}) => (store) => {
237
- if (!WebSocket) throw Error("client.websocket.unavailable");
238
- const socket = Socket(url, { onUnhandled, onStatusChange });
239
- let status = false;
240
- const statusWatcher = makeWatcher();
241
- function onUnhandled(id) {
242
- socket.stop(id, ["unwatch"]);
243
- }
244
- function onStatusChange(newStatus) {
245
- status = newStatus;
246
- statusWatcher.write({ status });
247
- }
248
- function once(op, payload, options) {
249
- return new Promise((resolve, reject) => {
250
- const id = socket.start(
251
- [op, pack(payload), getOptions(op, options) || {}],
252
- (error, result) => {
253
- socket.stop(id);
254
- error ? reject(Error(`server.${error}`)) : resolve(unpack(result));
255
- }
256
- );
257
- });
258
- }
259
- store.onWrite(connInfoPath, () => {
260
- status = socket.isAlive();
261
- return { status };
262
- });
263
- store.onRead(connInfoPath, () => ({ status }));
264
- store.onWatch(connInfoPath, () => statusWatcher.watch({ status }));
265
- store.on("read", (query, options) => once("read", query, options));
266
- store.on("write", (change, options) => once("write", change, options));
267
- store.on("watch", (query, options) => {
268
- if (watch === "none") throw Error("client.no_watch");
269
- const op = "watch";
270
- return makeStream((push, end) => {
271
- const id = socket.start(
272
- [op, pack(query), getOptions(op, options) || {}],
273
- (error, result) => {
274
- if (error) {
275
- socket.stop(id);
276
- end(Error(`server.${error}`));
277
- return;
278
- }
279
- push(unpack(result));
280
- }
281
- );
282
- return () => {
283
- socket.stop(id, ["unwatch"]);
284
- };
285
- });
286
- });
287
- };
288
- const WSRE = /^wss?:\/\//;
289
- function GraffyClient(baseUrl, options) {
290
- if (WSRE.test(baseUrl)) {
291
- return wsClient(baseUrl, options);
292
- }
293
- return httpClient(baseUrl, options);
294
- }
295
- export {
296
- GraffyClient as default
297
- };
File without changes
File without changes