@agentshouse/kit 0.2.1-c4ea88ba9424e → 0.2.1-c5d36e97b0fea

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/dist/copies.js CHANGED
@@ -32,6 +32,22 @@ function rebaseFixes(refusal) {
32
32
  function short(commit) {
33
33
  return commit.slice(0, 12);
34
34
  }
35
+ function moved(before, after) {
36
+ if (before.house !== after.house)
37
+ return [`${rootOf(after.room_handle)}: ${HOUSE_REF} is at ${short(after.house)}`];
38
+ return before.position === after.position ? [] : [`${rootOf(after.room_handle)}: read-only files refreshed`];
39
+ }
40
+ function expiredAuthority(error) {
41
+ if (!(error instanceof HouseRefusal))
42
+ return undefined;
43
+ try {
44
+ const { error: refused } = JSON.parse(error.text);
45
+ return refused?.code === 'position_expired' && typeof refused.authority === 'string' ? refused.authority : undefined;
46
+ }
47
+ catch {
48
+ return undefined;
49
+ }
50
+ }
35
51
  function covers(path, prefix) {
36
52
  return prefix === '' || path === prefix || path.startsWith(`${prefix}/`);
37
53
  }
@@ -152,7 +168,6 @@ export class LocalCopies {
152
168
  house;
153
169
  copies = new Map();
154
170
  queues = new Map();
155
- delivering = new Set();
156
171
  selecting = Promise.resolve();
157
172
  constructor(house) {
158
173
  this.house = house;
@@ -191,36 +206,62 @@ export class LocalCopies {
191
206
  .map((copy) => `${rootOf(copy.room_handle)}: ${copy.repository}`)
192
207
  .sort();
193
208
  }
194
- select() {
195
- const selected = this.selecting.then(() => this.reselect());
209
+ async select() {
210
+ const selected = await this.selected(() => this.fetched(false));
211
+ if (selected.refused)
212
+ logged(selected.text);
213
+ }
214
+ fetch() {
215
+ return this.selected(() => this.fetched(true));
216
+ }
217
+ selected(work) {
218
+ const selected = this.selecting.then(work);
196
219
  this.selecting = selected.catch(() => undefined);
197
220
  return selected;
198
221
  }
199
- async reselect() {
222
+ async fetched(refreshing) {
200
223
  const { local_copy: chosen } = await this.house.post('/kit/local-copy/selection', {});
201
- const selected = (chosen?.rooms ?? []).flatMap((room) => room.room_handle === null ? [] : [{ key: `rooms/${room.room_ref}`, handle: room.room_handle }]);
202
- const keys = new Set(selected.map((scope) => scope.key));
203
- const work = [];
204
- for (const key of this.copies.keys()) {
205
- if (keys.has(key))
206
- continue;
207
- work.push(this.exclusive(key, async () => {
208
- const copy = this.copies.get(key);
209
- if (copy.active)
210
- await this.save({ ...copy, active: false });
211
- }));
212
- }
213
- for (const scope of selected)
214
- work.push(this.exclusive(scope.key, () => this.apply(scope)).catch(logged));
215
- await Promise.all(work);
216
- }
217
- entries(frame) {
218
- const copy = [...this.copies.values()].find((held) => held.active && held.room_ref === frame.authority);
219
- if (copy === undefined)
220
- return;
221
- if (copy.log_epoch === frame.log_epoch && BigInt(frame.position) <= BigInt(copy.position))
222
- return;
223
- this.deliver(copy.key);
224
+ const selected = new Map(chosen.rooms.map((room) => [`rooms/${room.room_ref}`, room.room_handle]));
225
+ return this.holding([...new Set([...this.copies.keys(), ...selected.keys()])].sort(), async () => {
226
+ const said = [];
227
+ let refused = false;
228
+ for (const copy of this.copies.values()) {
229
+ if (selected.has(copy.key) || !copy.active)
230
+ continue;
231
+ await this.save({ ...copy, active: false });
232
+ said.push(`${rootOf(copy.room_handle)}: no longer synced to this Environment; its files stay`);
233
+ }
234
+ const current = [];
235
+ for (const [key, handle] of selected) {
236
+ try {
237
+ const held = this.copies.get(key);
238
+ if (held === undefined || !(await exists(held.repository))) {
239
+ said.push(await this.materialize(key, handle));
240
+ continue;
241
+ }
242
+ const copy = held.room_handle === handle ? held : await this.save({ ...held, room_handle: handle });
243
+ if (copy.active)
244
+ current.push(copy);
245
+ else
246
+ said.push(...moved(copy, await this.save(await this.received({ ...copy, active: true }))));
247
+ }
248
+ catch (error) {
249
+ refused = true;
250
+ said.push(`${rootOf(handle)}: ${causeOf(error)}`);
251
+ }
252
+ }
253
+ if (refreshing && current.length > 0) {
254
+ try {
255
+ const refreshed = await this.refreshed(current);
256
+ current.forEach((copy, index) => said.push(...moved(copy, refreshed[index])));
257
+ }
258
+ catch (error) {
259
+ refused = true;
260
+ said.push(error instanceof HouseRefusal ? (refusalOf(error.text)?.text ?? causeOf(error)) : causeOf(error));
261
+ }
262
+ }
263
+ return { refused, text: said.length === 0 ? 'Nothing new from House.' : said.join('\n') };
264
+ });
224
265
  }
225
266
  async observed(report) {
226
267
  const key = await this.keyAt(dirname(report.common));
@@ -270,16 +311,9 @@ export class LocalCopies {
270
311
  this.queues.set(key, queued.catch(() => undefined));
271
312
  return queued;
272
313
  }
273
- deliver(key) {
274
- if (this.delivering.has(key))
275
- return;
276
- this.delivering.add(key);
277
- void this.exclusive(key, async () => {
278
- this.delivering.delete(key);
279
- const copy = this.copies.get(key);
280
- if (copy.active)
281
- await this.save(await this.caughtUp(copy));
282
- }).catch(logged);
314
+ holding(keys, work) {
315
+ const [key, ...rest] = keys;
316
+ return key === undefined ? work() : this.exclusive(key, () => this.holding(rest, work));
283
317
  }
284
318
  async save(copy) {
285
319
  const path = join(kitHome(), 'local-copies', `${copy.key}.json`);
@@ -325,46 +359,13 @@ export class LocalCopies {
325
359
  }
326
360
  return read;
327
361
  }
328
- async apply(scope) {
329
- let copy = this.copies.get(scope.key);
330
- if (copy === undefined || !(await exists(copy.repository))) {
331
- await this.materialize(scope);
332
- return;
333
- }
334
- if (copy.room_handle !== scope.handle)
335
- copy = await this.save({ ...copy, room_handle: scope.handle });
336
- if (!copy.active) {
337
- await this.save(await this.received({ ...copy, active: true }));
338
- return;
339
- }
340
- const discovery = await this.house.post('/kit/door/discover', {
341
- prefixes: [rootOf(copy.room_handle)],
342
- });
343
- const authority = discovery.authorities.find((entry) => entry.room_ref === copy.room_ref);
344
- if (authority === undefined)
345
- return;
346
- if (authority.managed_replication !== 'admitted') {
347
- logged(`operation_denied: ${rootOf(copy.room_handle)} is not synchronized to this Environment`);
348
- return;
349
- }
350
- if (JSON.stringify([authority.operations.write, authority.protected_paths]) !==
351
- JSON.stringify([copy.write, copy.protected])) {
352
- await this.save(await this.received(copy));
353
- return;
354
- }
355
- await this.save(await this.caughtUp(copy));
356
- }
357
- async materialize(scope) {
358
- const read = await this.read(scope.handle);
359
- if (read === null) {
360
- logged(`House serves nothing of ${rootOf(scope.handle)} to copy`);
361
- return;
362
- }
362
+ async materialize(key, handle) {
363
+ const read = await this.read(handle);
364
+ if (read === null)
365
+ throw new Error('House serves nothing of it to copy');
363
366
  const repository = join(localCopies(), 'rooms', read.anchor);
364
- if (await exists(repository)) {
365
- logged(`${repository} already exists and House Kit leaves it as it is`);
366
- return;
367
- }
367
+ if (await exists(repository))
368
+ throw new Error(`${repository} already exists and House Kit leaves it as it is`);
368
369
  const staging = join(dirname(repository), `.${randomUUID()}`);
369
370
  await mkdir(staging, { recursive: true });
370
371
  let house;
@@ -372,7 +373,7 @@ export class LocalCopies {
372
373
  try {
373
374
  await git(staging, ['init', '-q', '-b', BRANCH]);
374
375
  house = await importCommits(staging, [
375
- { message: `House ${rootOf(scope.handle)}`, from: null, files: read.writable },
376
+ { message: `House ${rootOf(handle)}`, from: null, files: read.writable },
376
377
  ]);
377
378
  await git(staging, ['update-ref', `refs/heads/${BRANCH}`, house]);
378
379
  await git(staging, ['reset', '-q', '--hard']);
@@ -385,9 +386,9 @@ export class LocalCopies {
385
386
  throw error;
386
387
  }
387
388
  await this.save({
388
- key: scope.key,
389
+ key,
389
390
  room_ref: read.anchor,
390
- room_handle: scope.handle,
391
+ room_handle: handle,
391
392
  repository,
392
393
  active: true,
393
394
  house,
@@ -399,6 +400,7 @@ export class LocalCopies {
399
400
  manifests: { [house]: { parent: null, files: read.revisions } },
400
401
  moves: [],
401
402
  });
403
+ return `${rootOf(handle)}: copied to ${repository}`;
402
404
  }
403
405
  async received(copy) {
404
406
  const read = await this.read(copy.room_handle);
@@ -422,29 +424,52 @@ export class LocalCopies {
422
424
  await ignoreRules(copy.repository, [...read.writable.keys()], next);
423
425
  return next;
424
426
  }
425
- async caughtUp(start) {
426
- let copy = start;
427
- for (;;) {
427
+ async refreshed(copies) {
428
+ const discovery = await this.house.post('/kit/door/discover', {
429
+ prefixes: copies.map((copy) => rootOf(copy.room_handle)),
430
+ });
431
+ const regranted = new Map();
432
+ for (const copy of copies) {
433
+ const authority = discovery.authorities.find((entry) => entry.room_ref === copy.room_ref);
434
+ if (authority !== undefined &&
435
+ JSON.stringify([authority.operations.write, authority.protected_paths]) !== JSON.stringify([copy.write, copy.protected])) {
436
+ regranted.set(copy.key, await this.save(await this.received(copy)));
437
+ }
438
+ }
439
+ const caught = await this.caughtUp(copies.filter((copy) => !regranted.has(copy.key)), false);
440
+ return copies.map((copy) => regranted.get(copy.key) ?? caught.find((held) => held.key === copy.key));
441
+ }
442
+ async caughtUp(copies, whole) {
443
+ const caught = new Map(copies.map((copy) => [copy.key, copy]));
444
+ let asking = [...copies];
445
+ while (asking.length > 0) {
428
446
  let answer;
429
447
  try {
430
448
  answer = await this.house.post('/kit/door/enumerate', {
431
- prefixes: [rootOf(copy.room_handle)],
432
- positions: [{ authority: copy.room_ref, position: copy.position, log_epoch: copy.log_epoch }],
449
+ prefixes: asking.map((copy) => rootOf(copy.room_handle)),
450
+ positions: asking.map((copy) => ({ authority: copy.room_ref, position: copy.position, log_epoch: copy.log_epoch })),
433
451
  });
434
452
  }
435
453
  catch (error) {
436
- const code = error instanceof HouseRefusal ? (refusalOf(error.text)?.code ?? null) : null;
437
- if (code === 'position_expired')
438
- return this.received(copy);
439
- if (code !== 'replica_behind')
454
+ const expired = asking.find((copy) => copy.room_ref === expiredAuthority(error));
455
+ if (expired === undefined)
440
456
  throw error;
441
- return copy;
457
+ caught.set(expired.key, await this.save(await this.received(expired)));
458
+ asking = asking.filter((copy) => copy !== expired);
459
+ continue;
460
+ }
461
+ const advanced = [];
462
+ for (const copy of asking) {
463
+ const view = answer.authorities.find((entry) => entry.authority === copy.room_ref);
464
+ if (view === undefined || (view.position === copy.position && view.log_epoch === copy.log_epoch))
465
+ continue;
466
+ const next = await this.save(await this.incorporated(copy, view.changes, view.position, view.log_epoch));
467
+ caught.set(copy.key, next);
468
+ advanced.push(next);
442
469
  }
443
- const view = answer.authorities.find((entry) => entry.authority === copy.room_ref);
444
- if (view === undefined || (view.position === copy.position && view.log_epoch === copy.log_epoch))
445
- return copy;
446
- copy = await this.save(await this.incorporated(copy, view.changes, view.position, view.log_epoch));
470
+ asking = whole ? advanced : [];
447
471
  }
472
+ return copies.map((copy) => caught.get(copy.key));
448
473
  }
449
474
  async incorporated(copy, changes, position, logEpoch) {
450
475
  const houseTree = await tree(copy.repository, copy.house);
@@ -603,7 +628,7 @@ export class LocalCopies {
603
628
  const rebased = rebaseFixes(outcome.refused);
604
629
  let settled = await this.save(without(copy));
605
630
  try {
606
- settled = await this.save(await this.caughtUp(settled));
631
+ settled = (await this.caughtUp([settled], true))[0];
607
632
  }
608
633
  catch {
609
634
  return {
@@ -622,7 +647,7 @@ export class LocalCopies {
622
647
  }
623
648
  async recovered(copy) {
624
649
  const push = copy.push;
625
- const current = await this.save(await this.caughtUp(copy));
650
+ const current = (await this.caughtUp([copy], true))[0];
626
651
  const house = await tree(copy.repository, current.house);
627
652
  const root = rootOf(current.room_handle);
628
653
  if (!(await this.landed(copy.repository, push.changes, house, await tree(copy.repository, push.commit)))) {
@@ -666,7 +691,7 @@ export class LocalCopies {
666
691
  const push = copy.push;
667
692
  let written = push.written;
668
693
  if (push.changes.some((change) => change.op === 'rename')) {
669
- copy = await this.save(await this.caughtUp(copy));
694
+ copy = (await this.caughtUp([copy], true))[0];
670
695
  const house = await tree(copy.repository, copy.house);
671
696
  const sent = await tree(copy.repository, push.house);
672
697
  if (push.changes.some((change) => change.op === 'rename' && !house.has(relative(change.to)))) {
package/dist/end.js CHANGED
@@ -1,4 +1,4 @@
1
- import { killDescendants } from './acp.js';
1
+ import { killDescendants } from './scope.js';
2
2
  export const REPLACED = 'another Kit started for this Environment';
3
3
  export const SIGN_IN_AGAIN = 'sign in again';
4
4
  export function end(reason) {