@agentshouse/mdruntime 0.1.0-c389df724fd2f → 0.1.0-c38dcfe165d81

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.
Files changed (184) hide show
  1. package/README.md +24 -11
  2. package/dist/adapter.d.ts +175 -54
  3. package/dist/adapter.d.ts.map +1 -1
  4. package/dist/attached-model.d.ts +5 -1
  5. package/dist/attached-model.d.ts.map +1 -1
  6. package/dist/attached-model.js +38 -4
  7. package/dist/attempt.d.ts +16 -0
  8. package/dist/attempt.d.ts.map +1 -0
  9. package/dist/attempt.js +70 -0
  10. package/dist/binary.d.ts +18 -0
  11. package/dist/binary.d.ts.map +1 -0
  12. package/dist/binary.js +8 -0
  13. package/dist/bootstrap.d.ts +33 -5
  14. package/dist/bootstrap.d.ts.map +1 -1
  15. package/dist/bootstrap.js +202 -48
  16. package/dist/chain.d.ts +12 -0
  17. package/dist/chain.d.ts.map +1 -0
  18. package/dist/chain.js +64 -0
  19. package/dist/change-set.d.ts +9 -14
  20. package/dist/change-set.d.ts.map +1 -1
  21. package/dist/change-set.js +11 -56
  22. package/dist/change.d.ts +45 -0
  23. package/dist/change.d.ts.map +1 -1
  24. package/dist/change.js +34 -0
  25. package/dist/client.d.ts +37 -17
  26. package/dist/client.d.ts.map +1 -1
  27. package/dist/client.js +123 -22
  28. package/dist/collection.d.ts +13 -14
  29. package/dist/collection.d.ts.map +1 -1
  30. package/dist/collection.js +52 -56
  31. package/dist/compile.d.ts +11 -0
  32. package/dist/compile.d.ts.map +1 -0
  33. package/dist/compile.js +586 -0
  34. package/dist/connection.d.ts +59 -20
  35. package/dist/connection.d.ts.map +1 -1
  36. package/dist/connection.js +3810 -905
  37. package/dist/context.d.ts +2 -0
  38. package/dist/context.d.ts.map +1 -0
  39. package/dist/context.js +7 -0
  40. package/dist/conversation.d.ts +50 -20
  41. package/dist/conversation.d.ts.map +1 -1
  42. package/dist/conversation.js +714 -242
  43. package/dist/digest.d.ts +7 -0
  44. package/dist/digest.d.ts.map +1 -0
  45. package/dist/digest.js +113 -0
  46. package/dist/durability.d.ts +4 -1
  47. package/dist/durability.d.ts.map +1 -1
  48. package/dist/durability.js +5 -0
  49. package/dist/entry.d.ts +20 -0
  50. package/dist/entry.d.ts.map +1 -0
  51. package/dist/entry.js +159 -0
  52. package/dist/expectation.d.ts +21 -0
  53. package/dist/expectation.d.ts.map +1 -0
  54. package/dist/expectation.js +70 -0
  55. package/dist/failure.d.ts +13 -0
  56. package/dist/failure.d.ts.map +1 -0
  57. package/dist/failure.js +19 -0
  58. package/dist/fence.d.ts +6 -0
  59. package/dist/fence.d.ts.map +1 -0
  60. package/dist/fence.js +10 -0
  61. package/dist/file.d.ts +2 -19
  62. package/dist/file.d.ts.map +1 -1
  63. package/dist/file.js +7 -122
  64. package/dist/host.d.ts +11 -3
  65. package/dist/host.d.ts.map +1 -1
  66. package/dist/host.js +128 -25
  67. package/dist/identity.d.ts +3 -0
  68. package/dist/identity.d.ts.map +1 -1
  69. package/dist/identity.js +13 -1
  70. package/dist/index.d.ts +23 -9
  71. package/dist/index.d.ts.map +1 -1
  72. package/dist/index.js +11 -0
  73. package/dist/job.d.ts +14 -0
  74. package/dist/job.d.ts.map +1 -0
  75. package/dist/job.js +28 -0
  76. package/dist/live.d.ts +23 -0
  77. package/dist/live.d.ts.map +1 -0
  78. package/dist/live.js +127 -0
  79. package/dist/media.d.ts +8 -0
  80. package/dist/media.d.ts.map +1 -0
  81. package/dist/media.js +218 -0
  82. package/dist/memory.d.ts +19 -7
  83. package/dist/memory.d.ts.map +1 -1
  84. package/dist/memory.js +251 -98
  85. package/dist/messages.d.ts +28 -0
  86. package/dist/messages.d.ts.map +1 -0
  87. package/dist/messages.js +89 -0
  88. package/dist/model.d.ts +13 -6
  89. package/dist/model.d.ts.map +1 -1
  90. package/dist/model.js +8 -5
  91. package/dist/name.d.ts +4 -0
  92. package/dist/name.d.ts.map +1 -0
  93. package/dist/name.js +122 -0
  94. package/dist/observation.d.ts +22 -4
  95. package/dist/observation.d.ts.map +1 -1
  96. package/dist/observation.js +120 -14
  97. package/dist/open.d.ts +2 -0
  98. package/dist/open.d.ts.map +1 -0
  99. package/dist/open.js +1 -0
  100. package/dist/opened.d.ts +22 -0
  101. package/dist/opened.d.ts.map +1 -0
  102. package/dist/opened.js +107 -0
  103. package/dist/outcome.d.ts +72 -5
  104. package/dist/outcome.d.ts.map +1 -1
  105. package/dist/outcome.js +81 -1
  106. package/dist/overlay.d.ts +15 -5
  107. package/dist/overlay.d.ts.map +1 -1
  108. package/dist/overlay.js +136 -180
  109. package/dist/paths.d.ts +1 -0
  110. package/dist/paths.d.ts.map +1 -1
  111. package/dist/paths.js +4 -0
  112. package/dist/query.d.ts +42 -181
  113. package/dist/query.d.ts.map +1 -1
  114. package/dist/query.js +3 -461
  115. package/dist/reach.d.ts +18 -4
  116. package/dist/reach.d.ts.map +1 -1
  117. package/dist/reach.js +234 -69
  118. package/dist/react.d.ts +14 -8
  119. package/dist/react.d.ts.map +1 -1
  120. package/dist/react.js +8 -17
  121. package/dist/readiness.d.ts +50 -0
  122. package/dist/readiness.d.ts.map +1 -0
  123. package/dist/readiness.js +27 -0
  124. package/dist/recovery.d.ts +26 -0
  125. package/dist/recovery.d.ts.map +1 -0
  126. package/dist/recovery.js +224 -0
  127. package/dist/refresh.d.ts +20 -7
  128. package/dist/refresh.d.ts.map +1 -1
  129. package/dist/refresh.js +148 -59
  130. package/dist/replica.d.ts +59 -33
  131. package/dist/replica.d.ts.map +1 -1
  132. package/dist/replica.js +320 -157
  133. package/dist/room-draft.d.ts +59 -11
  134. package/dist/room-draft.d.ts.map +1 -1
  135. package/dist/room-draft.js +276 -80
  136. package/dist/rows.d.ts +23 -35
  137. package/dist/rows.d.ts.map +1 -1
  138. package/dist/rows.js +61 -117
  139. package/dist/save.d.ts +73 -4
  140. package/dist/save.d.ts.map +1 -1
  141. package/dist/save.js +825 -150
  142. package/dist/search.d.ts +15 -54
  143. package/dist/search.d.ts.map +1 -1
  144. package/dist/search.js +10 -121
  145. package/dist/seed.d.ts +16 -0
  146. package/dist/seed.d.ts.map +1 -0
  147. package/dist/seed.js +75 -0
  148. package/dist/shared.d.ts +15 -0
  149. package/dist/shared.d.ts.map +1 -0
  150. package/dist/shared.js +421 -0
  151. package/dist/status.d.ts +7 -2
  152. package/dist/status.d.ts.map +1 -1
  153. package/dist/status.js +3 -9
  154. package/dist/store.d.ts +107 -62
  155. package/dist/store.d.ts.map +1 -1
  156. package/dist/store.js +318 -354
  157. package/dist/stored.d.ts +129 -0
  158. package/dist/stored.d.ts.map +1 -0
  159. package/dist/stored.js +495 -0
  160. package/dist/tables.d.ts +326 -0
  161. package/dist/tables.d.ts.map +1 -0
  162. package/dist/tables.js +574 -0
  163. package/dist/transfer.d.ts +0 -5
  164. package/dist/transfer.d.ts.map +1 -1
  165. package/dist/turn.d.ts +9 -0
  166. package/dist/turn.d.ts.map +1 -0
  167. package/dist/turn.js +75 -0
  168. package/dist/validation.d.ts +29 -0
  169. package/dist/validation.d.ts.map +1 -0
  170. package/dist/validation.js +104 -0
  171. package/dist/windows.d.ts +11 -0
  172. package/dist/windows.d.ts.map +1 -0
  173. package/dist/windows.js +16 -0
  174. package/dist/wire.d.ts +169 -33
  175. package/dist/wire.d.ts.map +1 -1
  176. package/dist/wire.js +68 -31
  177. package/package.json +26 -15
  178. package/AGENTS.md +0 -253
  179. package/dist/attachment.d.ts +0 -3
  180. package/dist/attachment.d.ts.map +0 -1
  181. package/dist/attachment.js +0 -8
  182. package/dist/read-side.d.ts +0 -18
  183. package/dist/read-side.d.ts.map +0 -1
  184. package/dist/read-side.js +0 -433
package/dist/save.js CHANGED
@@ -1,15 +1,19 @@
1
1
  import { definitionOf } from './model.js';
2
- import { apply, ATTACHMENT, links, parse, pointerSegments, references, relative, relativize, retarget, serialize, target, } from '@agentshouse/mdmodel';
3
- import { immutable, } from './change.js';
4
- import { governing, parseCollection, validateCollection, } from './collection.js';
2
+ import { apply, ARRAY_INDEX, ATTACHMENT, links, parse, pointerSegments, references, relative, relativize, retarget, roomReferences, serialize, target, } from '@agentshouse/mdmodel';
3
+ import { immutable, leaving, queuedFile, departure, } from './change.js';
4
+ import { collectionOf, documentsOf, governing, } from './collection.js';
5
5
  import { changeBatchIdentity } from './identity.js';
6
- import { overlay } from './overlay.js';
7
- import { covered, normalize } from './paths.js';
8
- function refuse(rule, file, location) {
6
+ import { overlaying } from './overlay.js';
7
+ import { ancestors, covered, normalize } from './paths.js';
8
+ import { sorting } from './shared.js';
9
+ import { paced, ROW_WEIGHT, stepped } from './turn.js';
10
+ import { validatedCollection } from './validation.js';
11
+ function refuse(code, file, location) {
9
12
  return {
10
13
  kind: 'refused',
11
14
  refusal: {
12
- rule,
15
+ code,
16
+ retryable: false,
13
17
  ...(file === undefined ? {} : { file }),
14
18
  ...(location === undefined ? {} : { location }),
15
19
  },
@@ -19,7 +23,7 @@ function repointed(document, definition, from, to, moves, settled) {
19
23
  const parsed = parse(document.content, from);
20
24
  if (!parsed.ok)
21
25
  return [];
22
- let next = parsed.document;
26
+ const changes = [];
23
27
  const names = new Set();
24
28
  for (const { pointer, value } of definition === undefined
25
29
  ? []
@@ -31,38 +35,39 @@ function repointed(document, definition, from, to, moves, settled) {
31
35
  const destination = moves.get(named) ?? named;
32
36
  if (target(to, value) === destination)
33
37
  continue;
34
- const applied = apply(next, {
38
+ changes.push({
35
39
  kind: 'reference',
36
40
  pointer,
37
41
  value: retarget(value, relative(to, destination)),
38
42
  prior: value,
39
43
  });
40
- if (applied.outcome !== 'applied')
41
- continue;
42
- next = applied.document;
43
44
  names.add(name);
44
45
  }
46
+ if (changes.length === 0)
47
+ return [];
48
+ const applied = apply(parsed.document, changes);
49
+ if (applied.outcome !== 'applied')
50
+ return [];
51
+ const next = applied.document;
45
52
  return [...names].map((name) => ({ kind: 'field', path: from, name, value: next.fields[name] }));
46
53
  }
47
- function edited(document, path, changes) {
48
- let next = document;
54
+ function edited(document, changes) {
55
+ const parts = new Map();
49
56
  for (const change of changes) {
50
- if (change.path !== path)
51
- continue;
52
- if (change.kind !== 'section' && change.kind !== 'preamble')
53
- continue;
54
- const applied = apply(next, change.kind === 'preamble'
55
- ? { kind: 'preamble', value: change.value, prior: next.preamble }
56
- : {
57
+ if (change.kind === 'preamble')
58
+ parts.set('preamble', { kind: 'preamble', value: change.value, prior: document.preamble });
59
+ if (change.kind === 'section')
60
+ parts.set(`section ${change.name}`, {
57
61
  kind: 'section',
58
62
  name: change.name,
59
63
  value: change.value,
60
- prior: next.sections[change.name],
64
+ prior: document.sections[change.name] ?? '',
61
65
  });
62
- if (applied.outcome === 'applied')
63
- next = applied.document;
64
66
  }
65
- return next;
67
+ if (parts.size === 0)
68
+ return document;
69
+ const applied = apply(document, [...parts.values()]);
70
+ return applied.outcome === 'applied' ? applied.document : document;
66
71
  }
67
72
  function bodies(path, before, after) {
68
73
  const changes = [];
@@ -77,7 +82,7 @@ function bodies(path, before, after) {
77
82
  return changes;
78
83
  }
79
84
  function relinks(path, moved, before, moves) {
80
- let after = before;
85
+ const changes = [];
81
86
  for (const link of links(before, path)) {
82
87
  const destination = moves.get(link.target) ?? link.target;
83
88
  if (destination === link.target && moved === path)
@@ -85,19 +90,23 @@ function relinks(path, moved, before, moves) {
85
90
  const to = relative(moved, destination);
86
91
  if (to === relative(path, link.target))
87
92
  continue;
88
- const applied = apply(after, { kind: 'link', href: link.href, to });
89
- if (applied.outcome === 'refuse') {
90
- return {
91
- kind: 'refused',
92
- refusal: {
93
- rule: applied.refusal.rule,
94
- file: path,
95
- ...(applied.refusal.location === undefined ? {} : { location: applied.refusal.location }),
96
- },
97
- };
98
- }
99
- after = applied.document;
93
+ changes.push({ kind: 'link', href: link.href, to });
100
94
  }
95
+ if (changes.length === 0)
96
+ return { kind: 'changes', changes: [] };
97
+ const applied = apply(before, changes);
98
+ if (applied.outcome === 'refuse') {
99
+ return {
100
+ kind: 'refused',
101
+ refusal: {
102
+ code: applied.refusal.rule,
103
+ retryable: false,
104
+ file: path,
105
+ ...(applied.refusal.location === undefined ? {} : { location: applied.refusal.location }),
106
+ },
107
+ };
108
+ }
109
+ const after = applied.document;
101
110
  if (after === before)
102
111
  return { kind: 'changes', changes: [] };
103
112
  if (before.type === undefined) {
@@ -105,74 +114,161 @@ function relinks(path, moved, before, moves) {
105
114
  }
106
115
  return { kind: 'changes', changes: bodies(path, before, after) };
107
116
  }
108
- function subsumed(merged, path) {
109
- for (let index = merged.length - 1; index >= 0; index -= 1) {
110
- const item = merged[index];
111
- if (item.path !== path)
112
- continue;
113
- if (item.kind === 'section' || item.kind === 'preamble')
114
- merged.splice(index, 1);
115
- }
117
+ function partKey(change) {
118
+ return change.kind === 'section' ? `section ${change.name}` : change.kind;
116
119
  }
117
- function rewrites(changes, visible) {
120
+ function* rewrites(changes, visible) {
118
121
  const moves = new Map();
119
122
  const settled = new Set();
120
- for (const change of changes) {
123
+ const parts = new Map();
124
+ for (const [index, change] of changes.entries()) {
121
125
  if (change.kind === 'move')
122
126
  moves.set(change.path, change.to);
123
- if (change.kind === 'remove' || change.kind === 'replace' || change.kind === 'create') {
127
+ if (change.kind === 'remove' ||
128
+ change.kind === 'replace' ||
129
+ change.kind === 'create' ||
130
+ change.kind === 'binary') {
124
131
  settled.add(change.path);
125
132
  }
126
133
  if (change.kind === 'field')
127
134
  settled.add(`${change.path} ${change.name}`);
135
+ if (isPart(change)) {
136
+ const held = parts.get(change.path);
137
+ if (held === undefined)
138
+ parts.set(change.path, [index]);
139
+ else
140
+ held.push(index);
141
+ }
128
142
  }
129
143
  if (moves.size === 0)
130
144
  return { kind: 'changes', changes };
131
145
  const merged = [...changes];
132
146
  const added = [];
133
- for (const [path, document] of Object.entries(visible.collection.documents)) {
147
+ for (const [path, document] of yield* documentsOf(visible.collection)) {
134
148
  if (settled.has(path))
135
149
  continue;
136
150
  const moved = moves.get(path) ?? path;
137
151
  const definition = definitionOf(governing(visible.collection.composition, path), document.type);
138
- added.push(...repointed(document, definition, path, moved, moves, settled));
139
- const relinked = relinks(path, moved, edited(document, path, merged), moves);
152
+ const repointing = repointed(document, definition, path, moved, moves, settled);
153
+ added.push(...repointing);
154
+ const own = parts.get(path) ?? [];
155
+ const relinked = relinks(path, moved, edited(document, own.map((index) => merged[index])), moves);
140
156
  if (relinked.kind === 'refused')
141
157
  return relinked;
158
+ const keyed = new Map();
159
+ for (const index of own) {
160
+ const key = partKey(merged[index]);
161
+ if (!keyed.has(key))
162
+ keyed.set(key, index);
163
+ }
142
164
  for (const change of relinked.changes) {
143
165
  if (change.kind === 'replace') {
144
- subsumed(merged, path);
166
+ for (const index of own) {
167
+ const kind = merged[index]?.kind;
168
+ if (kind === 'section' || kind === 'preamble')
169
+ merged[index] = null;
170
+ }
145
171
  added.push(change);
146
172
  continue;
147
173
  }
148
- const index = merged.findIndex((item) => item.kind === change.kind &&
149
- item.path === path &&
150
- (change.kind !== 'section' || (item.kind === 'section' && item.name === change.name)));
151
- if (index === -1)
174
+ const index = keyed.get(partKey(change));
175
+ if (index === undefined)
152
176
  added.push(change);
153
177
  else
154
178
  merged[index] = change;
155
179
  }
180
+ const text = visible.collection.text[path].length;
181
+ const rewritten = repointing.length > 0 || relinked.changes.length > 0;
182
+ yield ROW_WEIGHT + text + (rewritten ? text : 0);
156
183
  }
157
- return { kind: 'changes', changes: [...added, ...merged] };
184
+ return {
185
+ kind: 'changes',
186
+ changes: [...added, ...merged.filter((change) => change !== null)],
187
+ };
158
188
  }
159
- function anticipated(changes, preconditions, rewritten, visible) {
160
- const derived = rewritten.filter((change) => !changes.includes(change));
189
+ function* anticipated(changes, preconditions, rewritten, visible) {
190
+ const given = new Set(changes);
191
+ const derived = rewritten.filter((change) => !given.has(change));
161
192
  return {
162
193
  changes: rewritten,
163
- preconditions: [...preconditionsOf(derived, visible, []), ...preconditions],
194
+ preconditions: [...(yield* preconditionsOf(derived, visible, [])), ...preconditions],
164
195
  };
165
196
  }
166
- export function providerRewritten(batch, visible) {
167
- const rewritten = rewrites(batch.changes, visible);
197
+ export function* providerRewritten(batch, visible) {
198
+ const rewritten = yield* rewrites(batch.changes, visible);
168
199
  if (rewritten.kind === 'refused')
169
200
  return batch;
170
201
  return {
171
202
  ...batch,
172
- ...anticipated(batch.changes, batch.preconditions, rewritten.changes, visible),
203
+ ...(yield* anticipated(batch.changes, batch.preconditions, rewritten.changes, visible)),
204
+ };
205
+ }
206
+ function crossed(document, definition, path, to) {
207
+ const changes = [];
208
+ for (const link of links(document, path)) {
209
+ const written = to(link.target, link.href);
210
+ if (written !== undefined)
211
+ changes.push({ kind: 'link', href: link.href, to: written });
212
+ }
213
+ for (const { pointer, value } of definition === undefined
214
+ ? []
215
+ : references(document, definition)) {
216
+ const named = target(path, value);
217
+ const written = named === null ? undefined : to(named, value);
218
+ if (written !== undefined) {
219
+ changes.push({ kind: 'reference', pointer, value: retarget(value, written), prior: value });
220
+ }
221
+ }
222
+ return changes;
223
+ }
224
+ function rewritten(path, document, changes) {
225
+ if (changes.length === 0)
226
+ return [];
227
+ const applied = apply(document, changes);
228
+ if (applied.outcome !== 'applied')
229
+ return [];
230
+ const after = applied.document;
231
+ if (document.type === undefined)
232
+ return [{ kind: 'replace', path, content: serialize(after) }];
233
+ const names = new Set(changes.flatMap((change) => change.kind === 'reference' ? [pointerSegments(change.pointer)[0] ?? ''] : []));
234
+ return [
235
+ ...[...names].map((name) => ({ kind: 'field', path, name, value: after.fields[name] })),
236
+ ...bodies(path, document, after),
237
+ ];
238
+ }
239
+ function roomPathOf(handle, path) {
240
+ return `/rooms/${handle}/${path}`;
241
+ }
242
+ export function* leavingRewritten(batch, visible) {
243
+ const between = batch.between;
244
+ if (between === undefined)
245
+ return batch;
246
+ const moved = new Map(between.moves.map(({ path, to }) => [path, roomPathOf(between.destination.handle, to)]));
247
+ const derived = [];
248
+ for (const [path, document] of yield* documentsOf(visible.collection)) {
249
+ if (moved.has(path))
250
+ continue;
251
+ const definition = definitionOf(governing(visible.collection.composition, path), document.type);
252
+ derived.push(...rewritten(path, document, crossed(document, definition, path, (named) => moved.get(named))));
253
+ yield ROW_WEIGHT + visible.collection.text[path].length;
254
+ }
255
+ if (derived.length === 0)
256
+ return batch;
257
+ return {
258
+ ...batch,
259
+ changes: [...derived, ...batch.changes],
260
+ preconditions: [...(yield* preconditionsOf(derived, visible, [])), ...batch.preconditions],
173
261
  };
174
262
  }
175
- function incoming(collection, removed) {
263
+ export function rewriting(batch, locator) {
264
+ return leaving(batch, locator) || batch.changes.some((change) => change.kind === 'move');
265
+ }
266
+ export function* effectiveIn(batch, locator, visible) {
267
+ return leaving(batch, locator)
268
+ ? yield* leavingRewritten(batch, visible)
269
+ : yield* providerRewritten(batch, visible);
270
+ }
271
+ function* incoming(collection, removed, candidates) {
176
272
  const found = new Map();
177
273
  const note = (target, file) => {
178
274
  if (!removed.has(target))
@@ -181,7 +277,20 @@ function incoming(collection, removed) {
181
277
  referrers.add(file);
182
278
  found.set(target, referrers);
183
279
  };
184
- for (const [path, document] of Object.entries(collection.documents)) {
280
+ const read = function* (paths) {
281
+ const seen = new Set();
282
+ for (const path of paths) {
283
+ if (seen.has(path))
284
+ continue;
285
+ seen.add(path);
286
+ const document = collection.document(path);
287
+ if (document !== undefined)
288
+ yield [path, document];
289
+ }
290
+ };
291
+ for (const [path, document] of candidates === null
292
+ ? yield* documentsOf(collection)
293
+ : read(candidates)) {
185
294
  const definition = definitionOf(governing(collection.composition, path), document.type);
186
295
  for (const { value, to } of definition === undefined ? [] : references(document, definition)) {
187
296
  if (to.includes(ATTACHMENT))
@@ -190,8 +299,9 @@ function incoming(collection, removed) {
190
299
  if (named !== null)
191
300
  note(named, path);
192
301
  }
302
+ yield ROW_WEIGHT + collection.text[path].length;
193
303
  }
194
- const [first] = [...found.keys()].sort();
304
+ const [first] = yield* sorting(found.keys());
195
305
  if (first === undefined)
196
306
  return null;
197
307
  const every = new Set();
@@ -200,18 +310,27 @@ function incoming(collection, removed) {
200
310
  every.add(file);
201
311
  return {
202
312
  kind: 'refused',
203
- refusal: { rule: 'referenced-missing-file', file: first, referrers: [...every].sort() },
313
+ refusal: {
314
+ code: 'referenced-missing-file',
315
+ retryable: false,
316
+ file: first,
317
+ referrers: yield* sorting(every),
318
+ },
204
319
  };
205
320
  }
206
321
  function affected(change) {
207
322
  return change.kind === 'move' ? [change.path, change.to] : [change.path];
208
323
  }
209
- function guard(changes, input) {
210
- const existing = new Set([
211
- ...Object.keys(input.visible.documents),
212
- ...input.visible.binaryTargets,
213
- ]);
214
- for (const change of changes) {
324
+ function guard(changes, input, removing) {
325
+ const overridden = new Map();
326
+ const existing = {
327
+ has: (path) => overridden.get(path) ??
328
+ (Object.hasOwn(input.visible.documents, path) || Object.hasOwn(input.visible.binaries, path)),
329
+ add: (path) => void overridden.set(path, true),
330
+ delete: (path) => void overridden.set(path, false),
331
+ };
332
+ const occupied = (path) => input.folders.has(path) ? path : (ancestors(path).find((one) => existing.has(one)) ?? null);
333
+ for (const [index, change] of changes.entries()) {
215
334
  for (const path of affected(change)) {
216
335
  const normalized = normalize(path);
217
336
  if (normalized === null || normalized === '')
@@ -219,22 +338,41 @@ function guard(changes, input) {
219
338
  if (covered(input.providerOwnedPaths, normalized)) {
220
339
  return refuse('provider-owned-path', normalized);
221
340
  }
222
- const operation = change.kind === 'remove' ? 'delete' : 'write';
223
- if (!covered(input.operations[operation], normalized))
341
+ const operation = change.kind === 'remove' ? removing : 'write';
342
+ if (!input.local && !covered(input.operations[operation], normalized))
224
343
  return refuse(`${operation}-grant`, normalized);
225
344
  }
226
- if (change.kind === 'create') {
227
- if (existing.has(change.path))
345
+ if (change.kind === 'create' || change.kind === 'arrival') {
346
+ const taken = existing.has(change.path) ? change.path : occupied(change.path);
347
+ if (taken !== null)
348
+ return refuse('path-collision', taken);
349
+ existing.add(change.path);
350
+ continue;
351
+ }
352
+ if (change.kind === 'binary') {
353
+ if (input.visible.documents[change.path] !== undefined ||
354
+ (existing.has(change.path) &&
355
+ input.bases.some((base) => base.kind === 'absent' && base.path === change.path))) {
228
356
  return refuse('path-collision', change.path);
357
+ }
358
+ const taken = occupied(change.path);
359
+ if (taken !== null)
360
+ return refuse('path-collision', taken);
361
+ if (!input.local &&
362
+ existing.has(change.path) &&
363
+ input.visible.revisions[departure(changes, index)] === undefined) {
364
+ return refuse('unknown-revision', change.path);
365
+ }
229
366
  existing.add(change.path);
230
367
  continue;
231
368
  }
232
369
  if (change.kind === 'move') {
233
370
  if (!existing.has(change.path))
234
371
  return refuse('path-absent', change.path);
235
- if (existing.has(change.to))
236
- return refuse('path-collision', change.to);
237
- if (unrevisionedDocument(input.visible, change.path)) {
372
+ const taken = existing.has(change.to) ? change.to : occupied(change.to);
373
+ if (taken !== null)
374
+ return refuse('path-collision', taken);
375
+ if (unrevised(input, change.path)) {
238
376
  return refuse('unknown-revision', change.path);
239
377
  }
240
378
  existing.delete(change.path);
@@ -244,30 +382,53 @@ function guard(changes, input) {
244
382
  if (!existing.has(change.path))
245
383
  return refuse('path-absent', change.path);
246
384
  if (change.kind === 'remove') {
247
- if (unrevisionedDocument(input.visible, change.path)) {
385
+ if (unrevised(input, change.path)) {
248
386
  return refuse('unknown-revision', change.path);
249
387
  }
250
388
  existing.delete(change.path);
251
389
  continue;
252
390
  }
253
391
  if (change.kind === 'replace') {
254
- if (input.visible.revisions[change.path] === undefined) {
392
+ if (unrevised(input, change.path)) {
255
393
  return refuse('unknown-revision', change.path);
256
394
  }
257
395
  continue;
258
396
  }
259
- if (input.visible.collection.documents[change.path] === undefined) {
397
+ if (input.visible.collection.document(change.path) === undefined) {
260
398
  return refuse('path-absent', change.path);
261
399
  }
262
400
  }
263
401
  return null;
264
402
  }
265
- function unrevisionedDocument(visible, path) {
266
- return visible.revisions[path] === undefined && !visible.binaryTargets.includes(path);
403
+ function unrevised(input, path) {
404
+ return (!input.local && input.queued?.has(path) !== true && input.visible.revisions[path] === undefined);
405
+ }
406
+ export function queuedDocuments(queue) {
407
+ const paths = new Set();
408
+ for (const batch of queue) {
409
+ for (const change of batch.changes) {
410
+ if (change.kind === 'binary' || (change.kind === 'arrival' && !('content' in change)))
411
+ continue;
412
+ if (change.kind === 'remove')
413
+ paths.delete(change.path);
414
+ else if (change.kind !== 'move')
415
+ paths.add(change.path);
416
+ else if (paths.delete(change.path))
417
+ paths.add(change.to);
418
+ }
419
+ }
420
+ return paths;
421
+ }
422
+ function identity(precondition) {
423
+ const part = precondition.kind === 'field' || precondition.kind === 'section' ? precondition.name : '';
424
+ return `${precondition.kind} ${precondition.path} ${part}`;
267
425
  }
268
426
  function existence(visible, path) {
269
427
  const revision = visible.revisions[path];
270
- return revision === undefined ? { kind: 'present', path } : { kind: 'revision', path, revision };
428
+ if (revision !== undefined)
429
+ return { kind: 'revision', path, revision };
430
+ const content = visible.documents[path];
431
+ return content === undefined ? { kind: 'present', path } : { kind: 'content', path, content };
271
432
  }
272
433
  function sameTarget(precondition, change) {
273
434
  if (precondition.kind !== change.kind || precondition.path !== change.path)
@@ -275,25 +436,25 @@ function sameTarget(precondition, change) {
275
436
  return ((precondition.kind !== 'field' && precondition.kind !== 'section') ||
276
437
  ((change.kind === 'field' || change.kind === 'section') && precondition.name === change.name));
277
438
  }
278
- function preconditionsOf(changes, visible, bases) {
439
+ function* preconditionsOf(changes, visible, bases) {
279
440
  const preconditions = [];
280
441
  const seen = new Set();
281
442
  const add = (precondition) => {
282
- const part = precondition.kind === 'field' || precondition.kind === 'section' ? precondition.name : '';
283
- const identity = `${precondition.kind} ${precondition.path} ${part}`;
284
- if (seen.has(identity))
443
+ const held = identity(precondition);
444
+ if (seen.has(held))
285
445
  return;
286
- seen.add(identity);
446
+ seen.add(held);
287
447
  preconditions.push(precondition);
288
448
  };
289
449
  for (const change of changes) {
450
+ yield ROW_WEIGHT;
290
451
  if (change.kind === 'field' || change.kind === 'section' || change.kind === 'preamble') {
291
452
  const based = bases.find((base) => sameTarget(base, change));
292
453
  if (based !== undefined) {
293
454
  add(based);
294
455
  continue;
295
456
  }
296
- const document = visible.collection.documents[change.path];
457
+ const document = visible.collection.document(change.path);
297
458
  if (change.kind === 'field') {
298
459
  add({
299
460
  kind: 'field',
@@ -318,6 +479,13 @@ function preconditionsOf(changes, visible, bases) {
318
479
  add({ kind: 'absent', path: change.path });
319
480
  continue;
320
481
  }
482
+ if (change.kind === 'binary') {
483
+ const revision = visible.revisions[change.path];
484
+ add(visible.binaries[change.path] === undefined || revision === undefined
485
+ ? { kind: 'absent', path: change.path }
486
+ : { kind: 'revision', path: change.path, revision });
487
+ continue;
488
+ }
321
489
  add(existence(visible, change.path));
322
490
  if (change.kind === 'move')
323
491
  add({ kind: 'absent', path: change.to });
@@ -327,10 +495,13 @@ function preconditionsOf(changes, visible, bases) {
327
495
  function depth(path) {
328
496
  return path.split('/').length;
329
497
  }
330
- function exceeded(limits, changes, resulting) {
331
- const written = changes.map((change) => (change.kind === 'move' ? change.to : change.path));
498
+ function* exceeding(limits, changes, effective, resulting) {
499
+ const written = effective.map((change) => (change.kind === 'move' ? change.to : change.path));
332
500
  const created = changes
333
- .filter((change) => change.kind === 'create' || change.kind === 'move')
501
+ .filter((change) => change.kind === 'create' ||
502
+ change.kind === 'move' ||
503
+ change.kind === 'binary' ||
504
+ change.kind === 'arrival')
334
505
  .map((change) => (change.kind === 'move' ? change.to : change.path));
335
506
  for (const limit of limits) {
336
507
  const value = limit.value;
@@ -344,9 +515,13 @@ function exceeded(limits, changes, resulting) {
344
515
  return refuse('provider-limit', undefined, limit.name);
345
516
  }
346
517
  if (limit.name === 'document-size') {
347
- const large = written.find((path) => (resulting.documents[path] ?? '').length > value);
348
- if (large !== undefined)
349
- return refuse('provider-limit', large, limit.name);
518
+ for (const path of written) {
519
+ const text = resulting.documents[path] ?? '';
520
+ if (new TextEncoder().encode(text).byteLength > value) {
521
+ return refuse('provider-limit', path, limit.name);
522
+ }
523
+ yield ROW_WEIGHT + text.length;
524
+ }
350
525
  }
351
526
  if (limit.name === 'path-length') {
352
527
  const long = created.find((path) => path.length > value);
@@ -376,7 +551,7 @@ function relativeField(path, name, value, handle, definition) {
376
551
  !opened.ok ||
377
552
  !JSON.stringify(value ?? null).includes(`/rooms/${handle}/`))
378
553
  return value;
379
- const held = apply(opened.document, { kind: 'field', name, value, prior: undefined });
554
+ const held = apply(opened.document, [{ kind: 'field', name, value, prior: undefined }]);
380
555
  if (held.outcome !== 'applied')
381
556
  return value;
382
557
  const result = relativize(held.document, path, handle, definition);
@@ -389,7 +564,7 @@ function relativeChanges(input) {
389
564
  return input.changes.map((change) => {
390
565
  const definition = typed(change.path);
391
566
  if (change.kind === 'field') {
392
- const type = visible.collection.documents[change.path]?.type;
567
+ const type = visible.collection.document(change.path)?.type;
393
568
  const value = relativeField(change.path, change.name, change.value, handle, definition(type));
394
569
  return value === change.value ? change : { ...change, value };
395
570
  }
@@ -412,65 +587,454 @@ function relativeChanges(input) {
412
587
  return { ...change, fields, sections };
413
588
  });
414
589
  }
415
- export function buildBatch(input) {
416
- const changes = relativeChanges(input);
417
- const structural = changes.find((change) => change.kind === 'move' || change.kind === 'remove');
418
- if (structural !== undefined &&
419
- Object.entries(input.visible.collection.documents).some(([path, document]) => document.type !== undefined &&
420
- document.type !== 'any' &&
421
- governing(input.composition, path).status === 'unavailable'))
422
- return refuse('model-unavailable', structural.path);
590
+ function unqueueable(file) {
591
+ return { kind: 'refused', refusal: { code: 'unknown-revision', retryable: false, file } };
592
+ }
593
+ function isPart(change) {
594
+ return change.kind === 'field' || change.kind === 'section' || change.kind === 'preamble';
595
+ }
596
+ function touched(change) {
597
+ return change.kind === 'move' ? [change.path, change.to] : [change.path];
598
+ }
599
+ function partsApplied(content, path, changes) {
600
+ const parsed = parse(content, path);
601
+ if (!parsed.ok)
602
+ return null;
603
+ const document = parsed.document;
604
+ const parts = new Map();
605
+ for (const change of changes) {
606
+ if (change.kind === 'field')
607
+ parts.set(`field ${change.name}`, {
608
+ kind: 'field',
609
+ name: change.name,
610
+ value: change.value,
611
+ prior: document.fields[change.name],
612
+ });
613
+ else if (change.kind === 'section')
614
+ parts.set(`section ${change.name}`, {
615
+ kind: 'section',
616
+ name: change.name,
617
+ value: change.value,
618
+ prior: document.sections[change.name] ?? '',
619
+ });
620
+ else
621
+ parts.set('preamble', { kind: 'preamble', value: change.value, prior: document.preamble });
622
+ }
623
+ const applied = apply(document, [...parts.values()]);
624
+ return applied.outcome === 'applied' ? serialize(applied.document) : null;
625
+ }
626
+ function settledParts(changes, deferred) {
627
+ for (const [held, parts] of deferred) {
628
+ const content = partsApplied(held.content, held.path, parts);
629
+ if (content === null)
630
+ return held.path;
631
+ changes[changes.indexOf(held)] = { ...held, content };
632
+ }
633
+ deferred.clear();
634
+ return null;
635
+ }
636
+ function folded(changes, change, appending, deferred) {
637
+ const held = changes.find((item) => item.path === change.path && (item.kind === 'create' || item.kind === 'replace'));
638
+ const whole = held === undefined ? -1 : changes.indexOf(held);
639
+ const occupied = changes.some((item) => !isPart(item) && item !== held && touched(item).includes(change.path));
640
+ if (isPart(change)) {
641
+ const same = changes.findIndex((item) => item.kind === change.kind &&
642
+ item.path === change.path &&
643
+ (change.kind === 'preamble' || ('name' in item && item.name === change.name)));
644
+ if (same !== -1) {
645
+ changes[same] = change;
646
+ return 'folded';
647
+ }
648
+ if (held === undefined) {
649
+ if (!appending)
650
+ return 'skipped';
651
+ if (occupied)
652
+ return 'refused';
653
+ changes.push(change);
654
+ return 'appended';
655
+ }
656
+ if ('content' in held) {
657
+ const parts = deferred.get(held);
658
+ if (parts === undefined)
659
+ deferred.set(held, [change]);
660
+ else
661
+ parts.push(change);
662
+ return 'folded';
663
+ }
664
+ if (change.kind === 'preamble')
665
+ return 'refused';
666
+ changes[whole] =
667
+ change.kind === 'field'
668
+ ? { ...held, fields: { ...held.fields, [change.name]: change.value } }
669
+ : { ...held, sections: { ...held.sections, [change.name]: change.value } };
670
+ return 'folded';
671
+ }
672
+ if (change.kind === 'replace') {
673
+ if (held !== undefined) {
674
+ if (!('content' in held))
675
+ return 'refused';
676
+ changes[whole] = { ...held, content: change.content };
677
+ return 'folded';
678
+ }
679
+ const parts = changes.filter((item) => isPart(item) && item.path === change.path);
680
+ if (parts.length === 0 && !appending)
681
+ return 'skipped';
682
+ if (occupied)
683
+ return 'refused';
684
+ changes.splice(0, changes.length, ...changes.filter((item) => !parts.includes(item)), change);
685
+ return 'appended';
686
+ }
687
+ if (changes.some((item) => touched(change).some((path) => touched(item).includes(path))))
688
+ return 'refused';
689
+ changes.push(change);
690
+ return 'appended';
691
+ }
692
+ function* relinked(batch, move, before, after, appending) {
693
+ const derived = yield* rewrites([move], after);
694
+ if (derived.kind === 'refused')
695
+ return derived;
696
+ const changes = [...batch.changes];
697
+ const appended = [];
698
+ let changed = false;
699
+ const deferred = new Map();
700
+ for (const change of derived.changes) {
701
+ if (change === move)
702
+ continue;
703
+ const unsettled = isPart(change) ? null : settledParts(changes, deferred);
704
+ if (unsettled !== null)
705
+ return unqueueable(unsettled);
706
+ const outcome = folded(changes, change, appending, deferred);
707
+ if (outcome === 'refused')
708
+ return unqueueable(change.path);
709
+ if (outcome === 'appended')
710
+ appended.push(change);
711
+ if (outcome !== 'skipped')
712
+ changed = true;
713
+ }
714
+ const unsettled = settledParts(changes, deferred);
715
+ if (unsettled !== null)
716
+ return unqueueable(unsettled);
717
+ if (!changed)
718
+ return { kind: 'relinked', batch };
719
+ const held = new Set(batch.preconditions.map(identity));
720
+ const added = yield* preconditionsOf(appended, before, []);
721
+ return {
722
+ kind: 'relinked',
723
+ batch: immutable({
724
+ ...batch,
725
+ id: changeBatchIdentity(),
726
+ changes,
727
+ preconditions: [
728
+ ...batch.preconditions,
729
+ ...added.filter((precondition) => !held.has(identity(precondition))),
730
+ ],
731
+ }),
732
+ };
733
+ }
734
+ function leading(changes, at) {
735
+ const from = departure(changes, at);
736
+ return new Set(changes.filter((change, index) => index < at && change.kind === 'move' && departure(changes, index) === from));
737
+ }
738
+ function created(batch, at) {
739
+ const file = batch.changes[at];
740
+ return (leading(batch.changes, at).size === 0 &&
741
+ batch.preconditions.some((precondition) => precondition.kind === 'absent' && precondition.path === file.path));
742
+ }
743
+ function withdrawnFile(batch, change, at) {
744
+ const file = batch.changes[at];
745
+ const from = departure(batch.changes, at);
746
+ const moves = leading(batch.changes, at);
747
+ const fresh = created(batch, at);
748
+ const vacated = new Set([...moves].map((move) => move.to));
749
+ if (fresh)
750
+ vacated.add(file.path);
751
+ const replaced = fresh
752
+ ? change.kind === 'remove'
753
+ ? []
754
+ : [{ ...file, path: change.to }]
755
+ : change.kind === 'remove'
756
+ ? [{ kind: 'remove', path: from }]
757
+ : change.to === from
758
+ ? [{ ...file, path: from }]
759
+ : [
760
+ { kind: 'move', path: from, to: change.to },
761
+ { ...file, path: change.to },
762
+ ];
763
+ const added = change.kind === 'move' && (fresh || change.to !== from)
764
+ ? [{ kind: 'absent', path: change.to }]
765
+ : [];
766
+ const changes = batch.changes.flatMap((item) => item === file ? replaced : item.kind === 'move' && moves.has(item) ? [] : [item]);
423
767
  if (changes.length === 0)
424
- return refuse('no-change');
425
- const rewritten = rewrites(changes, input.visible);
426
- if (rewritten.kind === 'refused')
427
- return rewritten;
428
- const refusal = guard(changes, input);
429
- if (refusal !== null)
430
- return refusal;
431
- const preconditions = preconditionsOf(changes, input.visible, input.bases);
432
- const predicted = anticipated(changes, preconditions, rewritten.changes, input.visible);
433
- const resulting = overlay(input.visible, [
434
- { id: '', baseRoomVersion: input.baseRoomVersion, ...predicted },
435
- ]);
436
- const removed = new Set(changes.filter((change) => change.kind === 'remove').map((change) => change.path));
768
+ return null;
769
+ const preconditions = [
770
+ ...added,
771
+ ...batch.preconditions.filter((precondition) => precondition.kind !== 'absent' || !vacated.has(precondition.path)),
772
+ ];
773
+ return immutable({ ...batch, id: changeBatchIdentity(), changes, preconditions });
774
+ }
775
+ export function* requeuing(queue, changes, visible, changeable, state) {
776
+ const next = [...queue];
777
+ const origins = new Map();
778
+ const withdrawn = new Map();
779
+ const owners = new Set();
780
+ const queued = new Set();
781
+ const kept = [];
782
+ const originOf = (batch) => origins.get(batch) ?? batch.id;
783
+ const open = (batch) => origins.has(batch) || changeable(batch);
784
+ for (const change of changes) {
785
+ const held = visible.binaries[change.path];
786
+ if ((change.kind !== 'move' && change.kind !== 'remove') || held?.state !== 'pending') {
787
+ kept.push(change);
788
+ continue;
789
+ }
790
+ let index = next.length - 1;
791
+ while (index >= 0 && queuedFile(next[index], change.path, held.localId) === -1)
792
+ index -= 1;
793
+ const owner = next[index];
794
+ if (owner === undefined || !open(owner))
795
+ return unqueueable(change.path);
796
+ const following = next.slice(index);
797
+ const rebuilt = [];
798
+ let before = change.kind === 'move' ? yield* state(next.slice(0, index)) : visible;
799
+ for (const [offset, batch] of following.entries()) {
800
+ let current = batch;
801
+ if (change.kind === 'move') {
802
+ const after = yield* state(next.slice(0, index + offset + 1));
803
+ const appending = offset === 0 && created(batch, queuedFile(batch, change.path, held.localId));
804
+ const relinking = yield* relinked(batch, change, before, after, appending);
805
+ if (relinking.kind === 'refused')
806
+ return relinking;
807
+ current = relinking.batch;
808
+ before = after;
809
+ }
810
+ if (current !== batch && (!open(batch) || batch.between !== undefined))
811
+ return unqueueable(change.path);
812
+ rebuilt.push(offset === 0
813
+ ? withdrawnFile(current, change, queuedFile(current, change.path, held.localId))
814
+ : current);
815
+ }
816
+ for (const [offset, batch] of following.entries()) {
817
+ const replaced = rebuilt[offset];
818
+ if (replaced === batch)
819
+ continue;
820
+ const origin = originOf(batch);
821
+ withdrawn.set(origin, replaced);
822
+ if (replaced !== null)
823
+ origins.set(replaced, origin);
824
+ }
825
+ owners.add(originOf(owner));
826
+ next.splice(index, following.length, ...rebuilt.filter((batch) => batch !== null));
827
+ queued.add(change.path);
828
+ }
829
+ return {
830
+ kind: 'requeued',
831
+ queue: withdrawn.size === 0 ? queue : next,
832
+ changes: kept,
833
+ queued,
834
+ withdrawn,
835
+ owners,
836
+ };
837
+ }
838
+ function covering(precondition) {
839
+ return precondition.kind === 'field' ||
840
+ precondition.kind === 'section' ||
841
+ precondition.kind === 'preamble'
842
+ ? identity(precondition)
843
+ : `path ${precondition.path}`;
844
+ }
845
+ export function joined(queue, owner, extra) {
846
+ const later = new Set(queue.slice(queue.indexOf(owner) + 1).flatMap((batch) => batch.changes.flatMap(touched)));
847
+ const clash = extra.changes.flatMap(touched).find((path) => later.has(path));
848
+ if (clash !== undefined)
849
+ return unqueueable(clash);
850
+ const changes = [...owner.changes];
851
+ const deferred = new Map();
852
+ for (const change of extra.changes) {
853
+ const unsettled = isPart(change) ? null : settledParts(changes, deferred);
854
+ if (unsettled !== null)
855
+ return unqueueable(unsettled);
856
+ if (folded(changes, change, true, deferred) === 'refused')
857
+ return unqueueable(change.path);
858
+ }
859
+ const unsettled = settledParts(changes, deferred);
860
+ if (unsettled !== null)
861
+ return unqueueable(unsettled);
862
+ const held = new Set(owner.preconditions.map(covering));
863
+ const written = new Set(owner.changes.flatMap((change) => change.kind === 'create' || change.kind === 'replace' ? [change.path] : []));
864
+ const preconditions = [
865
+ ...owner.preconditions,
866
+ ...extra.preconditions.filter((precondition) => {
867
+ const key = covering(precondition);
868
+ return (!held.has(key) && (key === `path ${precondition.path}` || !written.has(precondition.path)));
869
+ }),
870
+ ];
871
+ const batch = immutable({
872
+ ...owner,
873
+ ...(extra.validation === undefined ? {} : { validation: extra.validation }),
874
+ id: changeBatchIdentity(),
875
+ changes,
876
+ preconditions,
877
+ });
878
+ return { kind: 'joined', batch, queue: queue.map((item) => (item === owner ? batch : item)) };
879
+ }
880
+ export function* limiting(queue, batch, limits, state) {
881
+ const at = queue.indexOf(batch);
882
+ const rewritten = yield* providerRewritten(batch, yield* state(queue.slice(0, at)));
883
+ const limited = yield* exceeding(limits, batch.changes, rewritten.changes, yield* state(queue.slice(0, at + 1)));
884
+ return limited?.kind === 'refused' ? limited.refusal : null;
885
+ }
886
+ function typedText(content) {
887
+ if (content === undefined)
888
+ return false;
889
+ const parsed = parse(content);
890
+ return parsed.ok && parsed.document.type !== undefined && parsed.document.type !== 'any';
891
+ }
892
+ function touchedLibrary(resulting, touched, composition) {
893
+ const entries = touched
894
+ .flatMap((path) => {
895
+ const content = resulting.documents[path];
896
+ const binary = resulting.binaries[path] !== undefined;
897
+ return content === undefined && !binary ? [] : [{ path, content: content ?? null }];
898
+ })
899
+ .sort((left, right) => (left.path < right.path ? -1 : left.path > right.path ? 1 : 0));
900
+ const contents = new Map(entries.map(({ path, content }) => [path, content]));
901
+ const binaries = new Set(Object.keys(resulting.binaries).filter((path) => resulting.binaries[path] !== undefined));
902
+ return validatedCollection(() => Promise.resolve({ entries, next: null }), async (paths) => {
903
+ const known = new Map();
904
+ const pace = paced();
905
+ for (const path of paths) {
906
+ const content = contents.get(path);
907
+ if (binaries.has(path))
908
+ known.set(path, { type: undefined, binary: true });
909
+ else if (content !== undefined && content !== null) {
910
+ const read = parse(content, path);
911
+ if (read.ok)
912
+ known.set(path, { type: read.document.type ?? 'any', binary: false });
913
+ await pace(content.length + ROW_WEIGHT);
914
+ }
915
+ }
916
+ return known;
917
+ }, composition);
918
+ }
919
+ async function judging(visible, resulting, composition, validate, removed, touched) {
437
920
  if (removed.size > 0) {
438
- const referenced = incoming(parseCollection(resulting.documents, input.composition), removed);
921
+ const referenced = await stepped(incoming(collectionOf(resulting.documents, composition), removed, null));
439
922
  if (referenced !== null)
440
923
  return referenced;
441
924
  }
442
- const typed = (content) => {
443
- if (content === undefined)
444
- return false;
445
- const parsed = parse(content);
446
- return parsed.ok && parsed.document.type !== undefined && parsed.document.type !== 'any';
447
- };
448
- const typedPaths = [...new Set(predicted.changes.flatMap(affected))].filter((path) => typed(input.visible.documents[path]) || typed(resulting.documents[path]));
449
- const needsModel = typedPaths.length > 0;
450
- const withoutModel = typedPaths.find((path) => governing(input.composition, path).status === 'unavailable');
925
+ const typedPaths = [];
926
+ const pace = paced();
927
+ for (const path of touched) {
928
+ const before = visible.documents[path];
929
+ const after = resulting.documents[path];
930
+ if (typedText(before) || typedText(after))
931
+ typedPaths.push(path);
932
+ await pace((before?.length ?? 0) + (after?.length ?? 0) + ROW_WEIGHT);
933
+ }
934
+ const withoutModel = typedPaths.find((path) => governing(composition, path).status === 'unavailable');
451
935
  if (withoutModel !== undefined)
452
936
  return refuse('model-unavailable', withoutModel);
453
- const validationText = needsModel
454
- ? resulting.documents
455
- : Object.fromEntries(Object.entries(resulting.documents).filter(([path]) => predicted.changes.some((change) => affected(change).includes(path))));
456
- const verdict = validateCollection(validationText, resulting.binaryTargets, input.composition);
937
+ const verdict = typedPaths.length > 0
938
+ ? await validate(resulting, touched)
939
+ : await touchedLibrary(resulting, touched, composition);
457
940
  if (verdict.verdict === 'refuse') {
458
941
  return {
459
942
  kind: 'refused',
460
943
  refusal: {
461
- rule: verdict.rule,
944
+ code: verdict.rule,
945
+ retryable: false,
462
946
  file: verdict.file,
463
947
  ...(verdict.location === undefined ? {} : { location: verdict.location }),
464
948
  ...(verdict.referrers === undefined ? {} : { referrers: verdict.referrers }),
465
949
  },
466
950
  };
467
951
  }
468
- const limited = exceeded(input.providerLimits, changes, resulting);
952
+ return {
953
+ kind: 'judged',
954
+ validated: typedPaths
955
+ .map((path) => governing(composition, path))
956
+ .find((model) => model.status !== 'unavailable'),
957
+ };
958
+ }
959
+ function placed(reference) {
960
+ const key = roomPathOf(reference.handle, reference.path);
961
+ if (reference.kind === 'link')
962
+ return `link ${key}`;
963
+ if (reference.kind === 'item')
964
+ return `section ${reference.section} ${key}`;
965
+ const pointer = reference.pointer
966
+ .split('/')
967
+ .map((segment) => (ARRAY_INDEX.test(segment) ? '-' : segment))
968
+ .join('/');
969
+ return `field ${pointer} ${key}`;
970
+ }
971
+ function roomReferencesIn(path, content, composition) {
972
+ if (content === undefined)
973
+ return [];
974
+ const parsed = parse(content, path);
975
+ if (!parsed.ok)
976
+ return [];
977
+ return roomReferences(path, content, definitionOf(governing(composition, path), parsed.document.type));
978
+ }
979
+ function* admittedRooms(changes, visible, resulting, composition) {
980
+ const from = new Map();
981
+ for (const [index, change] of changes.entries()) {
982
+ if (change.kind === 'move')
983
+ from.set(change.to, departure(changes, index));
984
+ }
985
+ for (const path of [...new Set(changes.flatMap(affected))].sort()) {
986
+ const now = resulting.documents[path];
987
+ if (now === undefined)
988
+ continue;
989
+ const held = new Set(roomReferencesIn(path, visible.documents[from.get(path) ?? path], composition).map(placed));
990
+ for (const reference of roomReferencesIn(path, now, composition)) {
991
+ if (reference.kind === 'link' || reference.crossRoom === true)
992
+ continue;
993
+ if (held.has(placed(reference)))
994
+ continue;
995
+ return refuse('unresolved-reference', path, reference.kind === 'field' ? reference.pointer : reference.section);
996
+ }
997
+ yield ROW_WEIGHT + now.length;
998
+ }
999
+ return null;
1000
+ }
1001
+ function validationOf(validated) {
1002
+ return validated === undefined
1003
+ ? {}
1004
+ : {
1005
+ validation: {
1006
+ identity: validated.snapshot.identity,
1007
+ version: validated.snapshot.version,
1008
+ source: validated.status,
1009
+ },
1010
+ };
1011
+ }
1012
+ async function batching(input) {
1013
+ const changes = relativeChanges(input);
1014
+ const structural = changes.find((change) => change.kind === 'move' || change.kind === 'remove');
1015
+ if (structural !== undefined && input.unmodeled) {
1016
+ return refuse('model-unavailable', structural.path);
1017
+ }
1018
+ if (changes.length === 0)
1019
+ return refuse('no-change');
1020
+ const rewritten = await stepped(rewrites(changes, input.visible));
1021
+ if (rewritten.kind === 'refused')
1022
+ return rewritten;
1023
+ const refusal = guard(changes, input, 'delete');
1024
+ if (refusal !== null)
1025
+ return refusal;
1026
+ const preconditions = await stepped(preconditionsOf(changes, input.visible, input.bases));
1027
+ const predicted = await stepped(anticipated(changes, preconditions, rewritten.changes, input.visible));
1028
+ const resulting = await stepped(overlaying(input.visible, [{ id: '', baseRoomVersion: input.baseRoomVersion, ...predicted }]));
1029
+ const judged = await judging(input.visible, resulting, input.composition, input.validate, new Set(changes.filter((change) => change.kind === 'remove').map((change) => change.path)), [...new Set(predicted.changes.flatMap(affected))]);
1030
+ if (judged.kind === 'refused')
1031
+ return judged;
1032
+ const crossing = await stepped(admittedRooms(changes, input.visible, resulting, input.composition));
1033
+ if (crossing !== null)
1034
+ return crossing;
1035
+ const limited = await stepped(exceeding(input.providerLimits, changes, predicted.changes, resulting));
469
1036
  if (limited !== null)
470
1037
  return limited;
471
- const validated = typedPaths
472
- .map((path) => governing(input.composition, path))
473
- .find((model) => model.status !== 'unavailable');
474
1038
  return {
475
1039
  kind: 'batch',
476
1040
  batch: immutable({
@@ -479,15 +1043,126 @@ export function buildBatch(input) {
479
1043
  changes,
480
1044
  preconditions,
481
1045
  ...(input.writtenBy === undefined ? {} : { writtenBy: input.writtenBy }),
482
- ...(validated === undefined
483
- ? {}
484
- : {
485
- validation: {
486
- identity: validated.snapshot.identity,
487
- version: validated.snapshot.version,
488
- source: validated.status,
489
- },
490
- }),
1046
+ ...validationOf(judged.validated),
491
1047
  }),
492
1048
  };
493
1049
  }
1050
+ export const MOVE_DOCUMENTS = 'move-documents';
1051
+ function heldAt(visible, path) {
1052
+ return visible.documents[path] !== undefined || visible.binaries[path] !== undefined;
1053
+ }
1054
+ function arrivingText(path, to, source, moved) {
1055
+ const content = source.visible.documents[path];
1056
+ const document = source.visible.collection.document(path);
1057
+ if (document === undefined)
1058
+ return content;
1059
+ const definition = definitionOf(governing(source.composition, path), document.type);
1060
+ const changes = crossed(document, definition, path, (named, href) => {
1061
+ const at = moved.get(named);
1062
+ if (at !== undefined)
1063
+ return target(to, href) === at ? undefined : relative(to, at);
1064
+ return heldAt(source.visible, named) ? roomPathOf(source.handle, named) : undefined;
1065
+ });
1066
+ if (changes.length === 0)
1067
+ return content;
1068
+ const applied = apply(document, changes);
1069
+ return applied.outcome === 'applied' ? serialize(applied.document) : content;
1070
+ }
1071
+ function arrivalOf({ path, to }, source, moved) {
1072
+ const { documents, revisions, fileIds, changes, binaries } = source.visible;
1073
+ const kept = {
1074
+ ...(fileIds[path] === undefined ? {} : { fileId: fileIds[path] }),
1075
+ ...(changes[path] === undefined ? {} : { change: changes[path] }),
1076
+ };
1077
+ const heads = {
1078
+ ...(revisions[path] === undefined ? {} : { revision: revisions[path] }),
1079
+ ...kept,
1080
+ };
1081
+ const binary = binaries[path];
1082
+ if (binary?.state === 'accepted') {
1083
+ const { byteLength, mediaType, sha256 } = binary;
1084
+ return {
1085
+ kind: 'arrival',
1086
+ path: to,
1087
+ binary: { byteLength, mediaType, sha256 },
1088
+ ...heads,
1089
+ };
1090
+ }
1091
+ const content = arrivingText(path, to, source, moved);
1092
+ return {
1093
+ kind: 'arrival',
1094
+ path: to,
1095
+ content,
1096
+ ...(content === documents[path] ? heads : kept),
1097
+ };
1098
+ }
1099
+ async function moving(input) {
1100
+ const { source, destination, moves } = input;
1101
+ if (moves.length === 0)
1102
+ return refuse('no-change');
1103
+ const removals = moves.map(({ path }) => ({ kind: 'remove', path }));
1104
+ const leaves = guard(removals, { ...source, local: input.local, bases: [] }, 'write');
1105
+ if (leaves !== null)
1106
+ return leaves;
1107
+ const uploading = moves.find(({ path }) => source.visible.binaries[path]?.state === 'pending');
1108
+ if (uploading !== undefined)
1109
+ return refuse('unknown-revision', uploading.path);
1110
+ const moved = new Map(moves.map(({ path, to }) => [path, to]));
1111
+ const arrivals = moves.map((move) => arrivalOf(move, source, moved));
1112
+ const arrives = guard(arrivals, { ...destination, local: input.local, bases: [] }, 'write');
1113
+ if (arrives !== null)
1114
+ return arrives;
1115
+ const bound = input.providerLimits.find((limit) => limit.name === MOVE_DOCUMENTS)?.value;
1116
+ if (typeof bound === 'number' && moves.length > bound) {
1117
+ return refuse('provider-limit', undefined, MOVE_DOCUMENTS);
1118
+ }
1119
+ if (source.unmodeled)
1120
+ return refuse('model-unavailable', moves[0].path);
1121
+ const id = changeBatchIdentity();
1122
+ const between = {
1123
+ source: { room: source.room, baseRoomVersion: source.baseRoomVersion },
1124
+ destination: {
1125
+ room: destination.room,
1126
+ baseRoomVersion: destination.baseRoomVersion,
1127
+ handle: destination.handle,
1128
+ },
1129
+ moves,
1130
+ };
1131
+ const written = input.writtenBy === undefined ? {} : { writtenBy: input.writtenBy };
1132
+ const leavingBatch = {
1133
+ id,
1134
+ baseRoomVersion: source.baseRoomVersion,
1135
+ changes: removals,
1136
+ preconditions: moves.map(({ path }) => existence(source.visible, path)),
1137
+ ...written,
1138
+ between,
1139
+ };
1140
+ const effective = await stepped(leavingRewritten(leavingBatch, source.visible));
1141
+ const left = await stepped(overlaying(source.visible, [effective]));
1142
+ const sourceJudged = await judging(source.visible, left, source.composition, source.validate, new Set(moves.map(({ path }) => path)), [...new Set(effective.changes.map((change) => change.path))]);
1143
+ if (sourceJudged.kind === 'refused')
1144
+ return sourceJudged;
1145
+ const arrivingBatch = {
1146
+ id,
1147
+ baseRoomVersion: destination.baseRoomVersion,
1148
+ changes: arrivals,
1149
+ preconditions: moves.map(({ to }) => ({ kind: 'absent', path: to })),
1150
+ ...written,
1151
+ between,
1152
+ };
1153
+ const arrived = await stepped(overlaying(destination.visible, [arrivingBatch]));
1154
+ const destinationJudged = await judging(destination.visible, arrived, destination.composition, destination.validate, new Set(), moves.map(({ to }) => to));
1155
+ if (destinationJudged.kind === 'refused')
1156
+ return destinationJudged;
1157
+ const limited = (await stepped(exceeding(input.providerLimits, removals, effective.changes, left))) ??
1158
+ (await stepped(exceeding(input.providerLimits, arrivals, arrivals, arrived)));
1159
+ if (limited !== null)
1160
+ return limited;
1161
+ return { kind: 'moved', leaving: immutable(leavingBatch), arriving: immutable(arrivingBatch) };
1162
+ }
1163
+ export function buildMove(input) {
1164
+ return moving(input);
1165
+ }
1166
+ export function buildBatch(input) {
1167
+ return batching(input);
1168
+ }