replica-bridge 0.3.15 → 0.3.17
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/package.json +1 -1
- package/src/bridge/board.js +71 -10
- package/src/protocol.js +3 -1
package/package.json
CHANGED
package/src/bridge/board.js
CHANGED
|
@@ -23,7 +23,14 @@ const CATCH_UP_PAGES = 1000;
|
|
|
23
23
|
/** More changed tasks than this in one event, and the phone reads the whole board instead. */
|
|
24
24
|
export const CHANGED_ITEMS_MAX = 200;
|
|
25
25
|
/** What this bridge can do beyond reading, for board/state (PROTOCOL.md "看板"). */
|
|
26
|
-
export const BOARD_CAPS = ["post", "delta"] ;
|
|
26
|
+
export const BOARD_CAPS = ["post", "delta", "items", "tasks"] ;
|
|
27
|
+
/** Up to this many changed tasks in one event come with their contents (`task`); past it the phone reads them itself. */
|
|
28
|
+
export const CARRIED_TASKS_MAX = 50;
|
|
29
|
+
/** A task bigger than this is left for the phone to read. */
|
|
30
|
+
const CARRIED_TASK_MAX_BYTES = 256 * 1024;
|
|
31
|
+
|
|
32
|
+
/** What the backend says a changed task now is: its view, or gone (404). Missing when the read failed. */
|
|
33
|
+
|
|
27
34
|
|
|
28
35
|
/** The backend's own cap on a request body (agens-backend src/http.mjs BODY_MAX). */
|
|
29
36
|
export const POST_BODY_MAX_BYTES = 16 * 1024;
|
|
@@ -31,10 +38,11 @@ const REC = "[A-Za-z0-9_.:-]+";
|
|
|
31
38
|
|
|
32
39
|
/**
|
|
33
40
|
* GETs (#367). /session also writes TASK.md in the task's working folder, which is what 开工 asks for; /share and /me
|
|
34
|
-
* only read.
|
|
41
|
+
* only read. From 0.3.16 also one item, a task's source (`?offset=` pages its text), as the backend's own page opens it.
|
|
42
|
+
* Not /assist, /mcp, /search, /context, /changes (the bridge's own poll) or anything under /ui.
|
|
35
43
|
*/
|
|
36
44
|
const READS = [/^\/v1\/status$/, /^\/v1\/today$/, /^\/v1\/recommendations$/, new RegExp(`^/v1/recommendations/${REC}$`),
|
|
37
|
-
new RegExp(`^/v1/recommendations/${REC}/(share|session)$`), /^\/v1\/me
|
|
45
|
+
new RegExp(`^/v1/recommendations/${REC}/(share|session)$`), /^\/v1\/me$/, new RegExp(`^/v1/items/${REC}$`)];
|
|
38
46
|
|
|
39
47
|
/** POSTs (#367): a task's follow-up actions, cancelling its preparation note, and muting a conversation. Nothing else. */
|
|
40
48
|
const WRITES = [new RegExp(`^/v1/recommendations/${REC}/actions$`), new RegExp(`^/v1/recommendations/${REC}/prep/cancel$`), /^\/v1\/me\/muted$/];
|
|
@@ -110,10 +118,17 @@ const DEFAULT_STREAM_RETRY_SECONDS = 30;
|
|
|
110
118
|
|
|
111
119
|
|
|
112
120
|
/** Changes as a phone gets them: one per task, the latest, or a reset when there are too many. */
|
|
113
|
-
|
|
121
|
+
/** The latest change of each task, in the order they last changed. */
|
|
122
|
+
function latestOf(items ) {
|
|
114
123
|
const latest = new Map ();
|
|
115
124
|
for (const item of items) { latest.delete(item.recId); latest.set(item.recId, item); }
|
|
116
|
-
return
|
|
125
|
+
return [...latest.values()];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function changedEvent(items , carried = new Map ()) {
|
|
129
|
+
const latest = latestOf(items);
|
|
130
|
+
if (latest.length > CHANGED_ITEMS_MAX) return { t: "board/changed", reset: true };
|
|
131
|
+
return { t: "board/changed", items: latest.map((item) => ({ ...item, ...carried.get(item.recId) })) };
|
|
117
132
|
}
|
|
118
133
|
|
|
119
134
|
function changeOf(value ) {
|
|
@@ -135,6 +150,8 @@ export class BoardReader {
|
|
|
135
150
|
round = 0;
|
|
136
151
|
/** The backend's push stream: open, unknown (try it), or not there (an older backend answers 404). */
|
|
137
152
|
stream = null;
|
|
153
|
+
/** Changes go out one batch at a time and in order, each after its tasks were read. */
|
|
154
|
+
delivery = Promise.resolve();
|
|
138
155
|
streamSupported = null;
|
|
139
156
|
/** Not before this time (ms) after the backend refused a stream for a passing reason. */
|
|
140
157
|
streamRetryAt = 0;
|
|
@@ -215,6 +232,7 @@ export class BoardReader {
|
|
|
215
232
|
this.end = null;
|
|
216
233
|
this.up = null;
|
|
217
234
|
this.polling = false;
|
|
235
|
+
this.delivery = Promise.resolve();
|
|
218
236
|
this.round++;
|
|
219
237
|
}
|
|
220
238
|
|
|
@@ -259,20 +277,63 @@ export class BoardReader {
|
|
|
259
277
|
return;
|
|
260
278
|
}
|
|
261
279
|
if (round !== this.round || !this.subs.has(sub)) return;
|
|
280
|
+
// What changed comes with the tasks as they are now; reading them takes a moment, in which more may change, so
|
|
281
|
+
// read the log again up to where the shared reader got before this subscriber starts hearing from it.
|
|
282
|
+
let carried = new Map ();
|
|
283
|
+
if (!reset && missed.length) {
|
|
284
|
+
carried = await this.carry(missed);
|
|
285
|
+
try {
|
|
286
|
+
for (let page = 0; cursor !== null && cursor < this.end && page < CATCH_UP_PAGES; page++) {
|
|
287
|
+
const { items, next } = await this.changes(cursor);
|
|
288
|
+
missed.push(...items);
|
|
289
|
+
if (next === cursor && items.length === 0) break;
|
|
290
|
+
cursor = next;
|
|
291
|
+
}
|
|
292
|
+
} catch { /* the late ones go without contents; the phone reads them */ }
|
|
293
|
+
if (round !== this.round || !this.subs.has(sub)) return;
|
|
294
|
+
}
|
|
262
295
|
sub.admitting = false;
|
|
263
296
|
sub.cursor = Math.max(cursor ?? this.end , this.end );
|
|
264
297
|
sub.listener(this.state(true, sub.cursor));
|
|
265
298
|
if (reset) sub.listener({ t: "board/changed", reset: true });
|
|
266
|
-
else if (missed.length) sub.listener(changedEvent(missed));
|
|
299
|
+
else if (missed.length) sub.listener(changedEvent(missed, carried));
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Reads each changed task (its latest change only) from the backend, so the phone needs no read of its own. Only while
|
|
304
|
+
* a phone is subscribed, which is while its 任务 page is on screen, so marking them seen is what the phone's own read
|
|
305
|
+
* did before. Too many at once, and the phone reads them (or the whole board) itself.
|
|
306
|
+
*/
|
|
307
|
+
async carry(items ) {
|
|
308
|
+
const carried = new Map ();
|
|
309
|
+
const latest = latestOf(items);
|
|
310
|
+
if (latest.length > CARRIED_TASKS_MAX) return carried;
|
|
311
|
+
await Promise.all(latest.map(async ({ recId }) => {
|
|
312
|
+
const answer = await this.answer(`/v1/recommendations/${encodeURIComponent(recId)}`);
|
|
313
|
+
if ("error" in answer) return;
|
|
314
|
+
if (answer.status === 404) { carried.set(recId, { gone: true }); return; }
|
|
315
|
+
if (answer.status !== 200 || Buffer.byteLength(answer.body, "utf8") > CARRIED_TASK_MAX_BYTES) return;
|
|
316
|
+
try { carried.set(recId, { task: JSON.parse(answer.body) }); } catch { /* the phone reads it */ }
|
|
317
|
+
}));
|
|
318
|
+
return carried;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/** Reads the changed tasks, then tells each subscriber; batches stay in order. */
|
|
322
|
+
send(items , end ) {
|
|
323
|
+
const round = this.round;
|
|
324
|
+
this.delivery = this.delivery.then(async () => {
|
|
325
|
+
const carried = await this.carry(items);
|
|
326
|
+
if (round === this.round) this.deliver(items, end, carried);
|
|
327
|
+
});
|
|
267
328
|
}
|
|
268
329
|
|
|
269
330
|
/** New changes up to `end`: each subscriber gets those past its own place. */
|
|
270
|
-
deliver(items , end ) {
|
|
331
|
+
deliver(items , end , carried = new Map ()) {
|
|
271
332
|
for (const sub of this.subs) {
|
|
272
333
|
if (sub.cursor === null) continue;
|
|
273
334
|
const fresh = items.filter((item) => item.seq > sub.cursor );
|
|
274
335
|
sub.cursor = Math.max(sub.cursor, end);
|
|
275
|
-
if (fresh.length) sub.listener(changedEvent(fresh));
|
|
336
|
+
if (fresh.length) sub.listener(changedEvent(fresh, carried));
|
|
276
337
|
}
|
|
277
338
|
}
|
|
278
339
|
|
|
@@ -312,7 +373,7 @@ export class BoardReader {
|
|
|
312
373
|
if (up) this.end = end;
|
|
313
374
|
this.setUp(up);
|
|
314
375
|
if (!up) return;
|
|
315
|
-
if (items.length) this.
|
|
376
|
+
if (items.length) this.send(items, end );
|
|
316
377
|
for (const sub of this.subs) if (sub.cursor === null) void this.admit(sub);
|
|
317
378
|
if (this.streamSupported !== false && Date.now() >= this.streamRetryAt) void this.openStream(round);
|
|
318
379
|
}
|
|
@@ -363,7 +424,7 @@ export class BoardReader {
|
|
|
363
424
|
pending = [];
|
|
364
425
|
const end = Math.max(this.end ?? 0, ...batch.map((item) => item.seq));
|
|
365
426
|
this.end = end;
|
|
366
|
-
this.
|
|
427
|
+
this.send(batch, end);
|
|
367
428
|
};
|
|
368
429
|
try {
|
|
369
430
|
const decoder = new TextDecoder();
|
package/src/protocol.js
CHANGED
|
@@ -249,13 +249,15 @@ export class FrameCipher {
|
|
|
249
249
|
|
|
250
250
|
/** One task that changed, from agens-backend's change log (0.3.14). */
|
|
251
251
|
|
|
252
|
+
/** As a phone gets it from 0.3.17 on: the task as the backend now has it (`GET /v1/recommendations/:id`), or gone. */
|
|
253
|
+
|
|
252
254
|
/**
|
|
253
255
|
* What a subscriber hears. From 0.3.14 a board/state that says up also gives `seq` (where in the change log the phone
|
|
254
256
|
* starts), `caps` and the bridge's version, and board/changed names the tasks, or says `reset` when there are too many.
|
|
255
257
|
*/
|
|
256
258
|
|
|
257
259
|
|
|
258
|
-
|
|
260
|
+
|
|
259
261
|
|
|
260
262
|
|
|
261
263
|
/** One segment of a long line: `data` is base64 of bytes [segment * chunk size, …) of the payload, `size` its length. */
|