@agentshouse/mdruntime-adapter 0.1.0-cd59fd8945dcb → 0.1.0-cdfc733e397ee

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/read.js CHANGED
@@ -1,4 +1,4 @@
1
- import { readPosition, roomVersion } from './version.js';
1
+ import { DECIMAL, readPosition, roomVersion } from './version.js';
2
2
  export function authorityRoot(handle) {
3
3
  return `/rooms/${handle}`;
4
4
  }
@@ -51,8 +51,19 @@ export function headChange(held) {
51
51
  function headed(head) {
52
52
  return head === undefined ? {} : { head };
53
53
  }
54
- function declared(attachments) {
55
- return attachments === undefined ? {} : { attachments };
54
+ export function binaryFileOf(held) {
55
+ if (held.file === undefined)
56
+ return undefined;
57
+ const { file, bytes, media_type: mediaType, sha256 } = held;
58
+ if (typeof file !== 'string' ||
59
+ typeof bytes !== 'number' ||
60
+ typeof mediaType !== 'string' ||
61
+ typeof sha256 !== 'string')
62
+ return null;
63
+ return { fileRef: file, byteLength: bytes, mediaType, sha256 };
64
+ }
65
+ function binaried(binary) {
66
+ return binary === undefined ? {} : { binary };
56
67
  }
57
68
  function parseFile(value, mode) {
58
69
  if (typeof value !== 'object' || value === null)
@@ -61,16 +72,30 @@ function parseFile(value, mode) {
61
72
  if (typeof file.path !== 'string')
62
73
  return null;
63
74
  const head = headChange(file);
64
- if (head === null)
75
+ const binary = binaryFileOf(file);
76
+ if (head === null || binary === null)
65
77
  return null;
66
78
  if (mode === 'site') {
67
79
  if (typeof file.sha256 !== 'string')
68
80
  return null;
69
- return { path: file.path, revision: undefined, version: file.sha256, ...headed(head) };
81
+ return {
82
+ path: file.path,
83
+ revision: binary?.sha256,
84
+ version: file.sha256,
85
+ ...headed(head),
86
+ ...binaried(binary),
87
+ };
70
88
  }
71
89
  if (typeof file.revision !== 'string')
72
90
  return null;
73
- return { path: file.path, revision: file.revision, version: file.revision, ...headed(head) };
91
+ return {
92
+ path: file.path,
93
+ revision: file.revision,
94
+ version: file.revision,
95
+ ...(typeof file.sha256 === 'string' ? { sha256: file.sha256 } : {}),
96
+ ...headed(head),
97
+ ...binaried(binary),
98
+ };
74
99
  }
75
100
  function parseScope(value, mode) {
76
101
  if (typeof value !== 'object' || value === null)
@@ -106,17 +131,28 @@ function parseScope(value, mode) {
106
131
  return null;
107
132
  contents.push({ path: content.path, content: content.content });
108
133
  }
109
- if (mode === 'site')
110
- return { anchor: manifest.anchor, ...at, files, contents };
111
- return { anchor: manifest.anchor, ...at, files, contents, attachments: [] };
134
+ return { anchor: manifest.anchor, ...at, files, contents };
135
+ }
136
+ function parseCut(value) {
137
+ if (value === undefined)
138
+ return undefined;
139
+ if (typeof value !== 'object' || value === null)
140
+ return null;
141
+ const { position, log_epoch: logEpoch } = value;
142
+ if (typeof position !== 'string' || typeof logEpoch !== 'string')
143
+ return null;
144
+ return { position, log_epoch: logEpoch };
112
145
  }
113
146
  function parseCursor(value) {
114
147
  if (typeof value !== 'object' || value === null)
115
148
  return null;
116
- const { room, path } = value;
149
+ const { room, path, cut } = value;
117
150
  if (typeof room !== 'string' || (path !== null && typeof path !== 'string'))
118
151
  return null;
119
- return { room, path };
152
+ const parsed = parseCut(cut);
153
+ if (parsed === null)
154
+ return null;
155
+ return { room, path, ...(parsed === undefined ? {} : { cut: parsed }) };
120
156
  }
121
157
  export function parseEnvelope(body) {
122
158
  if (typeof body !== 'object' || body === null)
@@ -138,6 +174,18 @@ export function parseEnvelope(body) {
138
174
  export function parseSiteBootstrap(body) {
139
175
  return parseScope(body, 'site');
140
176
  }
177
+ export function siteBootstrapNext(body) {
178
+ if (typeof body !== 'object' || body === null || !('next' in body))
179
+ return null;
180
+ return typeof body.next === 'string' && body.next.length > 0 ? body.next : null;
181
+ }
182
+ export function siteBootstrapOffset(body) {
183
+ if (typeof body !== 'object' || body === null || !('offset' in body))
184
+ return null;
185
+ return Number.isSafeInteger(body.offset) && body.offset > 0
186
+ ? body.offset
187
+ : null;
188
+ }
141
189
  function parseChange(value, mode) {
142
190
  if (typeof value !== 'object' || value === null)
143
191
  return null;
@@ -147,8 +195,15 @@ function parseChange(value, mode) {
147
195
  if (held.removed === true)
148
196
  return { path: held.path, removed: true };
149
197
  const head = headChange(held);
150
- if (head === null)
198
+ const binary = binaryFileOf(held);
199
+ if (head === null || binary === null)
151
200
  return null;
201
+ if (binary !== undefined) {
202
+ if (mode === 'credential' && typeof held.revision !== 'string')
203
+ return null;
204
+ const revision = typeof held.revision === 'string' ? held.revision : binary.sha256;
205
+ return { path: held.path, binary, revision, version: revision, ...headed(head) };
206
+ }
152
207
  if (mode === 'site') {
153
208
  if (typeof held.content !== 'string' || typeof held.sha256 !== 'string')
154
209
  return null;
@@ -169,12 +224,6 @@ function parseChange(value, mode) {
169
224
  ...headed(head),
170
225
  };
171
226
  }
172
- if (typeof held.reference === 'object' && held.reference !== null) {
173
- const reference = held.reference;
174
- if (typeof reference.revision !== 'string')
175
- return null;
176
- return { path: held.path, reference: { revision: reference.revision } };
177
- }
178
227
  return null;
179
228
  }
180
229
  export function parseEnumeration(body, mode) {
@@ -199,14 +248,60 @@ export function parseEnumeration(body, mode) {
199
248
  return null;
200
249
  changes.push(mapped);
201
250
  }
202
- if (mode === 'site') {
203
- authorities.push({ authority: held.authority, ...at, changes });
204
- continue;
205
- }
206
- authorities.push({ authority: held.authority, ...at, changes, attachments: [] });
251
+ const next = held.next === undefined ? undefined : readPage(held.next);
252
+ if (next === null)
253
+ return null;
254
+ authorities.push({
255
+ authority: held.authority,
256
+ ...at,
257
+ changes,
258
+ ...(next === undefined ? {} : { next }),
259
+ });
207
260
  }
208
261
  return { authorities };
209
262
  }
263
+ function readPage(held) {
264
+ if (typeof held !== 'object' || held === null)
265
+ return null;
266
+ const { after, through, read } = held;
267
+ if (typeof after !== 'string' || !DECIMAL.test(after))
268
+ return null;
269
+ if (typeof through !== 'string' || !DECIMAL.test(through))
270
+ return null;
271
+ if (typeof read !== 'number' || !Number.isSafeInteger(read) || read < 0)
272
+ return null;
273
+ return { after, through, read };
274
+ }
275
+ function advances(asked, next) {
276
+ const moved = BigInt(next.through) - BigInt(asked.through);
277
+ return moved > 0n || (moved === 0n && next.after === asked.after && next.read > asked.read);
278
+ }
279
+ export async function wholeView(first, more) {
280
+ const changes = new Map(first.changes.map((change) => [change.path, change]));
281
+ let view = first;
282
+ while (view.next !== undefined) {
283
+ const asked = view.next;
284
+ const page = await more(asked);
285
+ if ('kind' in page)
286
+ return page;
287
+ if (page.position !== asked.through ||
288
+ page.logEpoch !== first.logEpoch ||
289
+ (page.next !== undefined && !advances(asked, page.next))) {
290
+ return { kind: 'transport-uncertainty' };
291
+ }
292
+ for (const change of page.changes) {
293
+ changes.delete(change.path);
294
+ changes.set(change.path, change);
295
+ }
296
+ view = page;
297
+ }
298
+ return {
299
+ authority: first.authority,
300
+ position: view.position,
301
+ logEpoch: first.logEpoch,
302
+ changes: [...changes.values()],
303
+ };
304
+ }
210
305
  export function advancedScope(scope, view) {
211
306
  const files = new Map(scope.files.map((file) => [file.path, file]));
212
307
  const contents = new Map(scope.contents.map((entry) => [entry.path, entry.content]));
@@ -215,18 +310,15 @@ export function advancedScope(scope, view) {
215
310
  contents.delete(change.path);
216
311
  if ('removed' in change)
217
312
  continue;
218
- if ('content' in change) {
219
- files.set(change.path, {
220
- path: change.path,
221
- revision: change.revision,
222
- version: change.version,
223
- ...headed(change.head),
224
- });
313
+ files.set(change.path, {
314
+ path: change.path,
315
+ revision: change.revision,
316
+ version: change.version,
317
+ ...headed(change.head),
318
+ ...('binary' in change ? { binary: change.binary } : {}),
319
+ });
320
+ if ('content' in change)
225
321
  contents.set(change.path, change.content);
226
- continue;
227
- }
228
- const { revision } = change.reference;
229
- files.set(change.path, { path: change.path, revision, version: revision });
230
322
  }
231
323
  return {
232
324
  ...scope,
@@ -244,16 +336,16 @@ export function bootstrapOf(scope) {
244
336
  const documents = {};
245
337
  const revisions = {};
246
338
  const changes = {};
247
- const binaryTargets = [];
339
+ const binaryFiles = {};
248
340
  for (const file of scope.files) {
249
341
  if (file.revision !== undefined)
250
342
  revisions[file.path] = file.revision;
251
343
  if (file.head !== undefined)
252
344
  changes[file.path] = file.head;
253
345
  const content = contents.get(file.path);
254
- if (content === undefined)
255
- binaryTargets.push(file.path);
256
- else
346
+ if (file.binary !== undefined)
347
+ binaryFiles[file.path] = file.binary;
348
+ else if (content !== undefined)
257
349
  documents[file.path] = content;
258
350
  }
259
351
  return {
@@ -262,8 +354,7 @@ export function bootstrapOf(scope) {
262
354
  documents,
263
355
  revisions,
264
356
  changes,
265
- binaryTargets: binaryTargets.sort(),
266
- ...declared(scope.attachments),
357
+ binaryFiles,
267
358
  };
268
359
  }
269
360
  export function schemaOf(scope, schema) {
@@ -284,23 +375,21 @@ export function refreshFrom(view, fromRoomVersion) {
284
375
  const documents = {};
285
376
  const revisions = {};
286
377
  const changes = {};
287
- const binaryTargets = [];
378
+ const binaryFiles = {};
288
379
  const removals = [];
289
380
  for (const change of view.changes) {
290
381
  if ('removed' in change) {
291
382
  removals.push(change.path);
292
383
  continue;
293
384
  }
294
- if ('content' in change) {
385
+ if ('content' in change)
295
386
  documents[change.path] = change.content;
296
- if (change.revision !== undefined)
297
- revisions[change.path] = change.revision;
298
- if (change.head !== undefined)
299
- changes[change.path] = change.head;
300
- continue;
301
- }
302
- binaryTargets.push(change.path);
303
- revisions[change.path] = change.reference.revision;
387
+ else
388
+ binaryFiles[change.path] = change.binary;
389
+ if (change.revision !== undefined)
390
+ revisions[change.path] = change.revision;
391
+ if (change.head !== undefined)
392
+ changes[change.path] = change.head;
304
393
  }
305
394
  return {
306
395
  kind: 'net-advancement',
@@ -308,8 +397,7 @@ export function refreshFrom(view, fromRoomVersion) {
308
397
  documents,
309
398
  revisions,
310
399
  changes,
311
- binaryTargets: binaryTargets.sort(),
400
+ binaryFiles,
312
401
  removals: removals.sort(),
313
- ...declared(view.attachments),
314
402
  };
315
403
  }
@@ -0,0 +1,7 @@
1
+ import type { TrackedAuthority } from './discovery.js';
2
+ export type HouseReferrer = {
3
+ locator: string;
4
+ referrer: Readonly<Record<string, string>>;
5
+ };
6
+ export declare function houseReferrer(referrer: unknown, tracked: ReadonlyMap<string, TrackedAuthority>): HouseReferrer | null;
7
+ //# sourceMappingURL=referrer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"referrer.d.ts","sourceRoot":"","sources":["../src/referrer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAEvD,MAAM,MAAM,aAAa,GAAG;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;CAAE,CAAC;AAgB5F,wBAAgB,aAAa,CAC3B,QAAQ,EAAE,OAAO,EACjB,OAAO,EAAE,WAAW,CAAC,MAAM,EAAE,gBAAgB,CAAC,GAC7C,aAAa,GAAG,IAAI,CAqBtB"}
@@ -0,0 +1,35 @@
1
+ function fields(value) {
2
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
3
+ ? value
4
+ : null;
5
+ }
6
+ function named(held, ...keys) {
7
+ const present = Object.keys(held);
8
+ return (present.length === keys.length &&
9
+ keys.every((key) => typeof held[key] === 'string' && held[key] !== ''));
10
+ }
11
+ export function houseReferrer(referrer, tracked) {
12
+ const held = fields(referrer);
13
+ if (held === null)
14
+ return null;
15
+ if (named(held, 'room', 'path')) {
16
+ const entry = tracked.get(held.room);
17
+ if (entry === undefined)
18
+ return null;
19
+ return {
20
+ locator: entry.locator,
21
+ referrer: { room_ref: entry.authority, path: held.path },
22
+ };
23
+ }
24
+ if (named(held, 'room_ref', 'record')) {
25
+ const roomRef = held.room_ref;
26
+ const entry = [...tracked.values()].find((candidate) => candidate.authority === roomRef);
27
+ return {
28
+ locator: entry?.locator ?? roomRef,
29
+ referrer: { room_ref: roomRef, record: held.record },
30
+ };
31
+ }
32
+ if (named(held, 'message'))
33
+ return { locator: '', referrer: { message: held.message } };
34
+ return null;
35
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"refusal.d.ts","sourceRoot":"","sources":["../src/refusal.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,cAAc,EACd,cAAc,EACd,eAAe,EACf,aAAa,EACd,MAAM,wBAAwB,CAAC;AAEhC,wBAAgB,WAAW,CAAC,IAAI,EAAE,OAAO,GAAG,eAAe,GAAG,IAAI,CAOjE;AAED,wBAAgB,aAAa,CAC3B,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,aAAa,CAAC,OAAO,CAAC,EAC7B,OAAO,EAAE,eAAe,GACvB,aAAa,CAEf;AAmBD,wBAAgB,YAAY,CAC1B,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,OAAO,EACb,OAAO,EAAE,MAAM,GACd,cAAc,GAAG,IAAI,CAMvB;AAED,wBAAgB,YAAY,CAC1B,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,OAAO,EACb,OAAO,EAAE,MAAM,GACd,cAAc,GAAG;IAAE,IAAI,EAAE,gBAAgB,CAAA;CAAE,GAAG,IAAI,CAGpD;AAED,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,cAAc,GAAG,IAAI,CAQrF"}
1
+ {"version":3,"file":"refusal.d.ts","sourceRoot":"","sources":["../src/refusal.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,cAAc,EACd,cAAc,EACd,eAAe,EACf,aAAa,EACd,MAAM,wBAAwB,CAAC;AAEhC,wBAAgB,WAAW,CAAC,IAAI,EAAE,OAAO,GAAG,eAAe,GAAG,IAAI,CAOjE;AAED,wBAAgB,aAAa,CAC3B,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,aAAa,CAAC,OAAO,CAAC,EAC7B,OAAO,EAAE,eAAe,GACvB,aAAa,CAEf;AAqBD,wBAAgB,YAAY,CAC1B,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,OAAO,EACb,OAAO,EAAE,MAAM,GACd,cAAc,GAAG,IAAI,CAMvB;AAED,wBAAgB,YAAY,CAC1B,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,OAAO,EACb,OAAO,EAAE,MAAM,GACd,cAAc,GAAG;IAAE,IAAI,EAAE,gBAAgB,CAAA;CAAE,GAAG,IAAI,CAGpD;AAED,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,cAAc,GAAG,IAAI,CAQrF"}
package/dist/refusal.js CHANGED
@@ -18,8 +18,9 @@ function doorFailure(status, body) {
18
18
  const refusal = doorRefusal(body);
19
19
  if (status === 401)
20
20
  return { kind: 'revoked-authorization' };
21
- if (refusal?.retryable === true)
21
+ if (refusal?.retryable === true || refusal?.code === 'update_required') {
22
22
  return { kind: 'transport-uncertainty' };
23
+ }
23
24
  if (status === 404 || refusal?.code === 'room_not_found')
24
25
  return { kind: 'room-not-readable' };
25
26
  if (status < 400)
@@ -1 +1 @@
1
- {"version":3,"file":"site.d.ts","sourceRoot":"","sources":["../src/site.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,OAAO,EAMR,MAAM,wBAAwB,CAAC;AAsBhC,OAAO,EAKL,KAAK,kBAAkB,EACxB,MAAM,gBAAgB,CAAC;AAgCxB,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,kBAAkB,GAAG,OAAO,CAwIxE"}
1
+ {"version":3,"file":"site.d.ts","sourceRoot":"","sources":["../src/site.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,OAAO,EAOR,MAAM,wBAAwB,CAAC;AA2BhC,OAAO,EAOL,KAAK,kBAAkB,EACxB,MAAM,gBAAgB,CAAC;AAmCxB,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,kBAAkB,GAAG,OAAO,CAuQxE"}
package/dist/site.js CHANGED
@@ -1,10 +1,12 @@
1
- import { actionOutcome } from './actions.js';
1
+ import { actionOutcome, SITE_SUBMIT } from './actions.js';
2
2
  import { mapDiscovery, parseSiteDiscovery, siteCapabilities, siteTracked, } from './discovery.js';
3
- import { scopeModels, streamedModels, viewModels } from './model.js';
4
- import { advancedScope, bootstrapOf, parseEnumeration, parseSiteBootstrap, refreshFrom, viewOf, } from './read.js';
3
+ import { partModels, streamedModels, viewModels } from './model.js';
4
+ import { advancedScope, bootstrapOf, parseEnumeration, parseSiteBootstrap, refreshFrom, siteBootstrapNext, siteBootstrapOffset, viewOf, wholeView, } from './read.js';
5
+ import { sha256Hex } from './digest.js';
5
6
  import { classifyDoor, classifyRead, discoveryFailure } from './refusal.js';
6
7
  import { houseNotifications } from './stream.js';
7
- import { SITE_DOOR_BOOTSTRAP, SITE_DOOR_DISCOVER, SITE_DOOR_ENUMERATE, SITE_DOOR_SUBMIT, } from './transport.js';
8
+ import { SITE_DOOR_BOOTSTRAP, SITE_DOOR_DISCOVER, SITE_DOOR_ENUMERATE, SITE_DOOR_SUBMIT, SITE_FILE_RECORD, SITE_FILES, } from './transport.js';
9
+ import { houseReferrer } from './referrer.js';
8
10
  import { followsVersion, logPosition, presented, reachedVersion, roomVersion, } from './version.js';
9
11
  const UNCERTAIN = { kind: 'transport-uncertainty' };
10
12
  function siteRoom(tracked) {
@@ -24,32 +26,94 @@ function settled(refused) {
24
26
  export function createSiteAdapter(transport) {
25
27
  const notifications = houseNotifications(transport, 'site');
26
28
  const tracked = new Map();
27
- async function enumeration(entry, from) {
29
+ function published(referrer) {
30
+ const named = houseReferrer(referrer, tracked);
31
+ return named === null || !tracked.has(named.locator) ? null : named;
32
+ }
33
+ async function enumerated(entry, from, page) {
28
34
  const answer = await transport.door(SITE_DOOR_ENUMERATE, {
29
- positions: [presented(entry.authority, from)],
35
+ positions: [
36
+ page === undefined
37
+ ? presented(entry.authority, from)
38
+ : {
39
+ ...presented(entry.authority, { position: page.after, logEpoch: from.logEpoch }),
40
+ page: { through: page.through, read: page.read },
41
+ },
42
+ ],
30
43
  });
31
44
  const classified = classifyRead(answer.status, answer.body, entry.locator);
32
45
  if (classified !== null)
33
46
  return classified;
34
- return parseEnumeration(answer.body, 'site') ?? UNCERTAIN;
47
+ const found = parseEnumeration(answer.body, 'site');
48
+ if (found === null)
49
+ return UNCERTAIN;
50
+ return viewOf(found, entry) ?? { kind: 'room-not-readable', locator: entry.locator };
51
+ }
52
+ async function enumeration(entry, from) {
53
+ const first = await enumerated(entry, from);
54
+ return 'kind' in first ? first : wholeView(first, (page) => enumerated(entry, from, page));
35
55
  }
36
- async function caughtUp(entry) {
37
- const answer = await transport.read(`${SITE_DOOR_BOOTSTRAP}${entry.authority}`);
56
+ async function bootstrapPart(entry, after, offset) {
57
+ const query = [
58
+ ...(after === null ? [] : [`after=${encodeURIComponent(after)}`]),
59
+ ...(offset === null ? [] : [`offset=${offset}`]),
60
+ ];
61
+ const answer = await transport.read(`${SITE_DOOR_BOOTSTRAP}${entry.authority}${query.length === 0 ? '' : `?${query.join('&')}`}`);
38
62
  const classified = classifyDoor(answer.status, answer.body, entry.locator);
39
63
  if (classified !== null)
40
64
  return { failure: classified };
41
65
  const scope = parseSiteBootstrap(answer.body);
42
- if (scope === null)
43
- return { failure: UNCERTAIN };
44
- if (roomVersion(scope) === roomVersion(entry))
45
- return { scope };
46
- const found = await enumeration(entry, scope);
47
- if ('kind' in found)
48
- return { failure: settled(found) };
49
- const view = viewOf(found, entry);
50
- if (view === undefined)
51
- return { failure: { kind: 'room-not-readable', locator: entry.locator } };
52
- return { scope: advancedScope(scope, view) };
66
+ return scope === null ? { failure: UNCERTAIN } : { body: answer.body, scope };
67
+ }
68
+ async function assembled(entry, scope, path, offset) {
69
+ const opened = scope.contents.at(-1);
70
+ if (opened === undefined || opened.path !== path || opened.content.length !== offset) {
71
+ return UNCERTAIN;
72
+ }
73
+ let text = opened.content;
74
+ for (;;) {
75
+ const part = await bootstrapPart(entry, path, text.length);
76
+ if ('failure' in part)
77
+ return part.failure;
78
+ const [piece] = part.scope.contents;
79
+ if (piece === undefined ||
80
+ part.scope.contents.length !== 1 ||
81
+ piece.path !== path ||
82
+ piece.content.length === 0) {
83
+ return UNCERTAIN;
84
+ }
85
+ text += piece.content;
86
+ const more = siteBootstrapOffset(part.body);
87
+ if (more === null)
88
+ break;
89
+ if (more !== text.length)
90
+ return UNCERTAIN;
91
+ }
92
+ const file = scope.files.find((held) => held.path === path);
93
+ if (file === undefined || (await sha256Hex(text)) !== file.version)
94
+ return UNCERTAIN;
95
+ return { ...scope, contents: [...scope.contents.slice(0, -1), { path, content: text }] };
96
+ }
97
+ async function caughtUp(entry, after) {
98
+ const part = await bootstrapPart(entry, after, null);
99
+ if ('failure' in part)
100
+ return part;
101
+ const next = siteBootstrapNext(part.body);
102
+ const offset = siteBootstrapOffset(part.body);
103
+ if (offset !== null) {
104
+ if (next === null)
105
+ return { failure: UNCERTAIN };
106
+ const scope = await assembled(entry, part.scope, next, offset);
107
+ return 'kind' in scope ? { failure: scope } : { scope, next };
108
+ }
109
+ const { scope } = part;
110
+ if (after !== null || next !== null || roomVersion(scope) === roomVersion(entry)) {
111
+ return { scope, next };
112
+ }
113
+ const view = await enumeration(entry, scope);
114
+ if ('kind' in view)
115
+ return { failure: settled(view) };
116
+ return { scope: advancedScope(scope, view), next: null };
53
117
  }
54
118
  async function refreshed(entry, fromRoomVersion) {
55
119
  const from = logPosition(fromRoomVersion);
@@ -60,12 +124,9 @@ export function createSiteAdapter(transport) {
60
124
  const models = await streamedModels(streamed.schemas, entry, 'site');
61
125
  return models === undefined ? streamed.page : { ...streamed.page, models };
62
126
  }
63
- const found = await enumeration(entry, from);
64
- if ('kind' in found)
65
- return found;
66
- const view = viewOf(found, entry);
67
- if (view === undefined)
68
- return { kind: 'room-not-readable', locator: entry.locator };
127
+ const view = await enumeration(entry, from);
128
+ if ('kind' in view)
129
+ return view;
69
130
  notifications.reach(entry.authority, view);
70
131
  const answer = refreshFrom(view, fromRoomVersion);
71
132
  if (answer.kind !== 'net-advancement')
@@ -75,6 +136,7 @@ export function createSiteAdapter(transport) {
75
136
  }
76
137
  return {
77
138
  localSaves: true,
139
+ exhaustiveDiscovery: true,
78
140
  async authorize() {
79
141
  return {};
80
142
  },
@@ -97,14 +159,24 @@ export function createSiteAdapter(transport) {
97
159
  const entry = tracked.get(request.locator);
98
160
  if (entry === undefined)
99
161
  return { kind: 'room-not-readable', locator: request.locator };
100
- if (request.pageToken !== undefined)
101
- return UNCERTAIN;
102
- const held = await caughtUp(entry);
162
+ const continued = request.pageToken === undefined
163
+ ? null
164
+ : JSON.parse(request.pageToken);
165
+ const held = await caughtUp(entry, continued?.after ?? null);
103
166
  if ('failure' in held)
104
167
  return held.failure;
105
168
  notifications.reach(entry.authority, held.scope);
106
- const models = await scopeModels(held.scope, entry);
107
- return { ...bootstrapOf(held.scope), ...(models === undefined ? {} : { models }) };
169
+ const seen = [
170
+ ...(continued?.seen ?? []),
171
+ ...entry.schemas.filter((schema) => held.scope.files.some((file) => file.path === schema)),
172
+ ];
173
+ const models = await partModels(held.scope, entry, new Set(seen), held.next === null);
174
+ const continuation = held.next === null ? null : { after: held.next, seen };
175
+ return {
176
+ ...bootstrapOf(held.scope),
177
+ ...(models === undefined ? {} : { models }),
178
+ ...(continuation === null ? {} : { pageToken: JSON.stringify(continuation) }),
179
+ };
108
180
  },
109
181
  async refresh(_authorization, request) {
110
182
  const entry = tracked.get(request.locator);
@@ -120,6 +192,32 @@ export function createSiteAdapter(transport) {
120
192
  async history() {
121
193
  return { kind: 'no-history' };
122
194
  },
195
+ async binary(_authorization, request) {
196
+ const named = published(request.kind === 'path' ? { room: request.locator, path: request.path } : request.referrer);
197
+ if (named === null) {
198
+ return { kind: 'rejected', code: 'referrer_unknown', fields: {}, retryable: false };
199
+ }
200
+ if (request.fileRef === undefined) {
201
+ return { kind: 'rejected', code: 'file_not_found', fields: {}, retryable: false };
202
+ }
203
+ const { room_ref, path, record } = named.referrer;
204
+ const read = await transport.download(`${SITE_FILES}${room_ref}/${encodeURIComponent(request.fileRef)}/${path === undefined
205
+ ? `${SITE_FILE_RECORD}/${encodeURIComponent(record)}`
206
+ : encodeURIComponent(path)}`);
207
+ if (read.status === 404) {
208
+ return { kind: 'rejected', code: 'file_not_found', fields: {}, retryable: false };
209
+ }
210
+ return read.status === 200 && read.blob !== null
211
+ ? { kind: 'binary', blob: read.blob }
212
+ : UNCERTAIN;
213
+ },
214
+ referrerScope(referrer) {
215
+ const named = published(referrer);
216
+ if (named === null)
217
+ return null;
218
+ const { path } = named.referrer;
219
+ return { kind: 'room', locator: named.locator, ...(path === undefined ? {} : { path }) };
220
+ },
123
221
  reached(locator, version, target) {
124
222
  return reachedVersion(version, target, tracked.get(locator)?.logEpoch);
125
223
  },
@@ -127,7 +225,7 @@ export function createSiteAdapter(transport) {
127
225
  return followsVersion(version, next);
128
226
  },
129
227
  async action(_authorization, request) {
130
- if (request.name !== 'submit') {
228
+ if (request.name !== SITE_SUBMIT) {
131
229
  return {
132
230
  kind: 'rejected',
133
231
  code: 'action-unimplemented',
@@ -137,7 +235,7 @@ export function createSiteAdapter(transport) {
137
235
  }
138
236
  if (!transport.online())
139
237
  return UNCERTAIN;
140
- return actionOutcome(await transport.door(SITE_DOOR_SUBMIT, request.input), 'submit');
238
+ return actionOutcome(await transport.door(SITE_DOOR_SUBMIT, request.input), SITE_SUBMIT);
141
239
  },
142
240
  roomStream: {
143
241
  hold(rooms) {
@@ -152,6 +250,11 @@ export function createSiteAdapter(transport) {
152
250
  },
153
251
  reopen: notifications.reopen,
154
252
  observe: notifications.observe,
253
+ published(locator, roomVersion) {
254
+ const at = logPosition(roomVersion);
255
+ if (at !== null)
256
+ notifications.published(locator, at);
257
+ },
155
258
  },
156
259
  serviceMode: notifications.serviceMode,
157
260
  };
package/dist/stream.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { ConversationStream, RefreshPage, RoomStreamEvent, ServiceModeSignal } from '@agentshouse/mdruntime';
1
+ import type { ConversationStream, RefreshPage, RoomStreamEvent, ServiceModeSignal, StreamReceipts } from '@agentshouse/mdruntime';
2
2
  import type { TrackedAuthority } from './discovery.js';
3
3
  import type { HouseDoor, HouseHeldRoom, HouseMode, HouseStreamEntry } from './transport.js';
4
4
  import { type LogPosition } from './version.js';
@@ -10,8 +10,9 @@ export type StreamedAdvancement = {
10
10
  };
11
11
  export type HouseNotifications = {
12
12
  runtime: string;
13
- observe(notify: (event: RoomStreamEvent) => void): () => void;
13
+ observe(notify: (event: RoomStreamEvent) => void, receipts: StreamReceipts): () => void;
14
14
  send(rooms: readonly HouseHeldRoom[]): void;
15
+ published(locator: string, at: LogPosition): void;
15
16
  reopen(): void;
16
17
  track(authorities: readonly TrackedAuthority[]): void;
17
18
  reach(authority: string, at: LogPosition): void;
@@ -1 +1 @@
1
- {"version":3,"file":"stream.d.ts","sourceRoot":"","sources":["../src/stream.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAEV,kBAAkB,EAElB,WAAW,EACX,eAAe,EAEf,iBAAiB,EAElB,MAAM,wBAAwB,CAAC;AAEhC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAEvD,OAAO,KAAK,EACV,SAAS,EACT,aAAa,EAEb,SAAS,EAET,gBAAgB,EAEjB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAe,KAAK,WAAW,EAAE,MAAM,cAAc,CAAC;AAgB7D,MAAM,MAAM,YAAY,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC;AAEvE,MAAM,MAAM,mBAAmB,GAAG;IAChC,IAAI,EAAE,WAAW,CAAC;IAClB,OAAO,EAAE,SAAS,gBAAgB,EAAE,CAAC;IACrC,OAAO,EAAE,WAAW,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;CAChD,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE,eAAe,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;IAC9D,IAAI,CAAC,KAAK,EAAE,SAAS,aAAa,EAAE,GAAG,IAAI,CAAC;IAC5C,MAAM,IAAI,IAAI,CAAC;IACf,KAAK,CAAC,WAAW,EAAE,SAAS,gBAAgB,EAAE,GAAG,IAAI,CAAC;IACtD,KAAK,CAAC,SAAS,EAAE,MAAM,EAAE,EAAE,EAAE,WAAW,GAAG,IAAI,CAAC;IAChD,OAAO,CACL,OAAO,EAAE,gBAAgB,EACzB,IAAI,EAAE,WAAW,EACjB,KAAK,CAAC,EAAE,YAAY,GACnB,mBAAmB,GAAG,IAAI,CAAC;IAC9B,aAAa,EAAE,kBAAkB,CAAC;IAClC,WAAW,EAAE,iBAAiB,CAAC;CAChC,CAAC;AAuBF,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,SAAS,EAAE,IAAI,EAAE,SAAS,GAAG,kBAAkB,CAyS5F"}
1
+ {"version":3,"file":"stream.d.ts","sourceRoot":"","sources":["../src/stream.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAGV,kBAAkB,EAElB,WAAW,EACX,eAAe,EAEf,iBAAiB,EACjB,cAAc,EAEf,MAAM,wBAAwB,CAAC;AAGhC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAEvD,OAAO,KAAK,EACV,SAAS,EACT,aAAa,EAEb,SAAS,EAET,gBAAgB,EAEjB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAe,KAAK,WAAW,EAAE,MAAM,cAAc,CAAC;AA0C7D,MAAM,MAAM,YAAY,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC;AAEvE,MAAM,MAAM,mBAAmB,GAAG;IAChC,IAAI,EAAE,WAAW,CAAC;IAClB,OAAO,EAAE,SAAS,gBAAgB,EAAE,CAAC;IACrC,OAAO,EAAE,WAAW,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;CAChD,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE,eAAe,KAAK,IAAI,EAAE,QAAQ,EAAE,cAAc,GAAG,MAAM,IAAI,CAAC;IACxF,IAAI,CAAC,KAAK,EAAE,SAAS,aAAa,EAAE,GAAG,IAAI,CAAC;IAC5C,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,WAAW,GAAG,IAAI,CAAC;IAClD,MAAM,IAAI,IAAI,CAAC;IACf,KAAK,CAAC,WAAW,EAAE,SAAS,gBAAgB,EAAE,GAAG,IAAI,CAAC;IACtD,KAAK,CAAC,SAAS,EAAE,MAAM,EAAE,EAAE,EAAE,WAAW,GAAG,IAAI,CAAC;IAChD,OAAO,CACL,OAAO,EAAE,gBAAgB,EACzB,IAAI,EAAE,WAAW,EACjB,KAAK,CAAC,EAAE,YAAY,GACnB,mBAAmB,GAAG,IAAI,CAAC;IAC9B,aAAa,EAAE,kBAAkB,CAAC;IAClC,WAAW,EAAE,iBAAiB,CAAC;CAChC,CAAC;AAkFF,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,SAAS,EAAE,IAAI,EAAE,SAAS,GAAG,kBAAkB,CAmhB5F"}