@agentshouse/mdruntime 0.1.0-cd3486bd400ed → 0.1.0-cd37f80239af0

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 (201) hide show
  1. package/README.md +21 -9
  2. package/dist/adapter.d.ts +149 -46
  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.map +1 -1
  8. package/dist/attempt.js +14 -1
  9. package/dist/binary.d.ts +18 -0
  10. package/dist/binary.d.ts.map +1 -0
  11. package/dist/binary.js +8 -0
  12. package/dist/bootstrap.d.ts +27 -11
  13. package/dist/bootstrap.d.ts.map +1 -1
  14. package/dist/bootstrap.js +184 -84
  15. package/dist/chain.d.ts +11 -0
  16. package/dist/chain.d.ts.map +1 -0
  17. package/dist/chain.js +64 -0
  18. package/dist/change-set.d.ts +9 -14
  19. package/dist/change-set.d.ts.map +1 -1
  20. package/dist/change-set.js +11 -62
  21. package/dist/change.d.ts +41 -0
  22. package/dist/change.d.ts.map +1 -1
  23. package/dist/change.js +34 -0
  24. package/dist/client.d.ts +31 -13
  25. package/dist/client.d.ts.map +1 -1
  26. package/dist/client.js +103 -7
  27. package/dist/collection.d.ts +10 -7
  28. package/dist/collection.d.ts.map +1 -1
  29. package/dist/collection.js +38 -79
  30. package/dist/compile.d.ts +16 -0
  31. package/dist/compile.d.ts.map +1 -0
  32. package/dist/compile.js +786 -0
  33. package/dist/connection.d.ts +50 -16
  34. package/dist/connection.d.ts.map +1 -1
  35. package/dist/connection.js +3786 -931
  36. package/dist/conversation.d.ts +37 -16
  37. package/dist/conversation.d.ts.map +1 -1
  38. package/dist/conversation.js +700 -257
  39. package/dist/digest.d.ts +7 -0
  40. package/dist/digest.d.ts.map +1 -0
  41. package/dist/digest.js +113 -0
  42. package/dist/durability.d.ts +4 -1
  43. package/dist/durability.d.ts.map +1 -1
  44. package/dist/durability.js +5 -0
  45. package/dist/entry.d.ts +20 -0
  46. package/dist/entry.d.ts.map +1 -0
  47. package/dist/entry.js +151 -0
  48. package/dist/fence.d.ts +6 -0
  49. package/dist/fence.d.ts.map +1 -0
  50. package/dist/fence.js +10 -0
  51. package/dist/file.d.ts +2 -19
  52. package/dist/file.d.ts.map +1 -1
  53. package/dist/file.js +7 -122
  54. package/dist/fulltext.d.ts +27 -0
  55. package/dist/fulltext.d.ts.map +1 -0
  56. package/dist/fulltext.js +370 -0
  57. package/dist/host.d.ts +13 -3
  58. package/dist/host.d.ts.map +1 -1
  59. package/dist/host.js +130 -17
  60. package/dist/identity.d.ts +2 -0
  61. package/dist/identity.d.ts.map +1 -1
  62. package/dist/identity.js +6 -0
  63. package/dist/index.d.ts +17 -9
  64. package/dist/index.d.ts.map +1 -1
  65. package/dist/index.js +7 -0
  66. package/dist/live.d.ts +22 -0
  67. package/dist/live.d.ts.map +1 -0
  68. package/dist/live.js +123 -0
  69. package/dist/media.d.ts +8 -0
  70. package/dist/media.d.ts.map +1 -0
  71. package/dist/media.js +218 -0
  72. package/dist/memory.d.ts +17 -7
  73. package/dist/memory.d.ts.map +1 -1
  74. package/dist/memory.js +231 -91
  75. package/dist/messages.d.ts +28 -0
  76. package/dist/messages.d.ts.map +1 -0
  77. package/dist/messages.js +90 -0
  78. package/dist/model.d.ts +9 -2
  79. package/dist/model.d.ts.map +1 -1
  80. package/dist/model.js +8 -2
  81. package/dist/name.d.ts +4 -0
  82. package/dist/name.d.ts.map +1 -0
  83. package/dist/name.js +122 -0
  84. package/dist/observation.d.ts +20 -2
  85. package/dist/observation.d.ts.map +1 -1
  86. package/dist/observation.js +113 -12
  87. package/dist/opened.d.ts +22 -0
  88. package/dist/opened.d.ts.map +1 -0
  89. package/dist/opened.js +107 -0
  90. package/dist/outcome.d.ts +52 -4
  91. package/dist/outcome.d.ts.map +1 -1
  92. package/dist/outcome.js +46 -0
  93. package/dist/overlay.d.ts +15 -5
  94. package/dist/overlay.d.ts.map +1 -1
  95. package/dist/overlay.js +129 -177
  96. package/dist/paths.d.ts +1 -0
  97. package/dist/paths.d.ts.map +1 -1
  98. package/dist/paths.js +4 -0
  99. package/dist/query.d.ts +68 -4
  100. package/dist/query.d.ts.map +1 -1
  101. package/dist/query.js +48 -7
  102. package/dist/reach.d.ts +18 -4
  103. package/dist/reach.d.ts.map +1 -1
  104. package/dist/reach.js +224 -55
  105. package/dist/react.d.ts +9 -6
  106. package/dist/react.d.ts.map +1 -1
  107. package/dist/react.js +7 -17
  108. package/dist/readiness.d.ts +52 -0
  109. package/dist/readiness.d.ts.map +1 -0
  110. package/dist/readiness.js +27 -0
  111. package/dist/recovery.d.ts +26 -0
  112. package/dist/recovery.d.ts.map +1 -0
  113. package/dist/recovery.js +224 -0
  114. package/dist/refresh.d.ts +17 -14
  115. package/dist/refresh.d.ts.map +1 -1
  116. package/dist/refresh.js +136 -56
  117. package/dist/replica.d.ts +58 -33
  118. package/dist/replica.d.ts.map +1 -1
  119. package/dist/replica.js +307 -159
  120. package/dist/room-draft.d.ts +51 -4
  121. package/dist/room-draft.d.ts.map +1 -1
  122. package/dist/room-draft.js +210 -18
  123. package/dist/rows.d.ts +37 -35
  124. package/dist/rows.d.ts.map +1 -1
  125. package/dist/rows.js +173 -102
  126. package/dist/save.d.ts +70 -3
  127. package/dist/save.d.ts.map +1 -1
  128. package/dist/save.js +805 -147
  129. package/dist/search.d.ts +39 -28
  130. package/dist/search.d.ts.map +1 -1
  131. package/dist/search.js +37 -105
  132. package/dist/seed.d.ts +16 -0
  133. package/dist/seed.d.ts.map +1 -0
  134. package/dist/seed.js +75 -0
  135. package/dist/shared.d.ts +15 -0
  136. package/dist/shared.d.ts.map +1 -0
  137. package/dist/shared.js +421 -0
  138. package/dist/status.d.ts +5 -0
  139. package/dist/status.d.ts.map +1 -1
  140. package/dist/status.js +3 -1
  141. package/dist/store.d.ts +107 -80
  142. package/dist/store.d.ts.map +1 -1
  143. package/dist/store.js +325 -461
  144. package/dist/stored.d.ts +130 -0
  145. package/dist/stored.d.ts.map +1 -0
  146. package/dist/stored.js +560 -0
  147. package/dist/tables.d.ts +325 -0
  148. package/dist/tables.d.ts.map +1 -0
  149. package/dist/tables.js +578 -0
  150. package/dist/transfer.d.ts +0 -5
  151. package/dist/transfer.d.ts.map +1 -1
  152. package/dist/turn.d.ts +9 -0
  153. package/dist/turn.d.ts.map +1 -0
  154. package/dist/turn.js +75 -0
  155. package/dist/validation.d.ts +29 -0
  156. package/dist/validation.d.ts.map +1 -0
  157. package/dist/validation.js +101 -0
  158. package/dist/windows.d.ts +22 -0
  159. package/dist/windows.d.ts.map +1 -0
  160. package/dist/windows.js +89 -0
  161. package/dist/wire.d.ts +165 -30
  162. package/dist/wire.d.ts.map +1 -1
  163. package/dist/wire.js +56 -29
  164. package/dist/words.d.ts +19 -0
  165. package/dist/words.d.ts.map +1 -0
  166. package/dist/words.js +156 -0
  167. package/package.json +26 -15
  168. package/AGENTS.md +0 -290
  169. package/dist/attachment.d.ts +0 -3
  170. package/dist/attachment.d.ts.map +0 -1
  171. package/dist/attachment.js +0 -8
  172. package/dist/chat/acts.d.ts +0 -34
  173. package/dist/chat/acts.d.ts.map +0 -1
  174. package/dist/chat/acts.js +0 -36
  175. package/dist/chat/messages.d.ts +0 -21
  176. package/dist/chat/messages.d.ts.map +0 -1
  177. package/dist/chat/messages.js +0 -151
  178. package/dist/chat/overlay.d.ts +0 -31
  179. package/dist/chat/overlay.d.ts.map +0 -1
  180. package/dist/chat/overlay.js +0 -97
  181. package/dist/chat/parts.d.ts +0 -57
  182. package/dist/chat/parts.d.ts.map +0 -1
  183. package/dist/chat/parts.js +0 -119
  184. package/dist/chat/process.d.ts +0 -20
  185. package/dist/chat/process.d.ts.map +0 -1
  186. package/dist/chat/process.js +0 -32
  187. package/dist/chat/provider.d.ts +0 -57
  188. package/dist/chat/provider.d.ts.map +0 -1
  189. package/dist/chat/provider.js +0 -637
  190. package/dist/chat/questions.d.ts +0 -31
  191. package/dist/chat/questions.d.ts.map +0 -1
  192. package/dist/chat/questions.js +0 -111
  193. package/dist/chat/read.d.ts +0 -5
  194. package/dist/chat/read.d.ts.map +0 -1
  195. package/dist/chat/read.js +0 -13
  196. package/dist/chat.d.ts +0 -7
  197. package/dist/chat.d.ts.map +0 -1
  198. package/dist/chat.js +0 -2
  199. package/dist/read-side.d.ts +0 -18
  200. package/dist/read-side.d.ts.map +0 -1
  201. package/dist/read-side.js +0 -433
package/dist/save.js CHANGED
@@ -1,10 +1,13 @@
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 { collectionOf, documentsOf, governing, 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';
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';
8
11
  function refuse(code, file, location) {
9
12
  return {
10
13
  kind: 'refused',
@@ -20,7 +23,7 @@ function repointed(document, definition, from, to, moves, settled) {
20
23
  const parsed = parse(document.content, from);
21
24
  if (!parsed.ok)
22
25
  return [];
23
- let next = parsed.document;
26
+ const changes = [];
24
27
  const names = new Set();
25
28
  for (const { pointer, value } of definition === undefined
26
29
  ? []
@@ -32,38 +35,39 @@ function repointed(document, definition, from, to, moves, settled) {
32
35
  const destination = moves.get(named) ?? named;
33
36
  if (target(to, value) === destination)
34
37
  continue;
35
- const applied = apply(next, {
38
+ changes.push({
36
39
  kind: 'reference',
37
40
  pointer,
38
41
  value: retarget(value, relative(to, destination)),
39
42
  prior: value,
40
43
  });
41
- if (applied.outcome !== 'applied')
42
- continue;
43
- next = applied.document;
44
44
  names.add(name);
45
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;
46
52
  return [...names].map((name) => ({ kind: 'field', path: from, name, value: next.fields[name] }));
47
53
  }
48
- function edited(document, path, changes) {
49
- let next = document;
54
+ function edited(document, changes) {
55
+ const parts = new Map();
50
56
  for (const change of changes) {
51
- if (change.path !== path)
52
- continue;
53
- if (change.kind !== 'section' && change.kind !== 'preamble')
54
- continue;
55
- const applied = apply(next, change.kind === 'preamble'
56
- ? { kind: 'preamble', value: change.value, prior: next.preamble }
57
- : {
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}`, {
58
61
  kind: 'section',
59
62
  name: change.name,
60
63
  value: change.value,
61
- prior: next.sections[change.name],
64
+ prior: document.sections[change.name] ?? '',
62
65
  });
63
- if (applied.outcome === 'applied')
64
- next = applied.document;
65
66
  }
66
- 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;
67
71
  }
68
72
  function bodies(path, before, after) {
69
73
  const changes = [];
@@ -78,7 +82,7 @@ function bodies(path, before, after) {
78
82
  return changes;
79
83
  }
80
84
  function relinks(path, moved, before, moves) {
81
- let after = before;
85
+ const changes = [];
82
86
  for (const link of links(before, path)) {
83
87
  const destination = moves.get(link.target) ?? link.target;
84
88
  if (destination === link.target && moved === path)
@@ -86,20 +90,23 @@ function relinks(path, moved, before, moves) {
86
90
  const to = relative(moved, destination);
87
91
  if (to === relative(path, link.target))
88
92
  continue;
89
- const applied = apply(after, { kind: 'link', href: link.href, to });
90
- if (applied.outcome === 'refuse') {
91
- return {
92
- kind: 'refused',
93
- refusal: {
94
- code: applied.refusal.rule,
95
- retryable: false,
96
- file: path,
97
- ...(applied.refusal.location === undefined ? {} : { location: applied.refusal.location }),
98
- },
99
- };
100
- }
101
- after = applied.document;
93
+ changes.push({ kind: 'link', href: link.href, to });
102
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;
103
110
  if (after === before)
104
111
  return { kind: 'changes', changes: [] };
105
112
  if (before.type === undefined) {
@@ -107,74 +114,161 @@ function relinks(path, moved, before, moves) {
107
114
  }
108
115
  return { kind: 'changes', changes: bodies(path, before, after) };
109
116
  }
110
- function subsumed(merged, path) {
111
- for (let index = merged.length - 1; index >= 0; index -= 1) {
112
- const item = merged[index];
113
- if (item.path !== path)
114
- continue;
115
- if (item.kind === 'section' || item.kind === 'preamble')
116
- merged.splice(index, 1);
117
- }
117
+ function partKey(change) {
118
+ return change.kind === 'section' ? `section ${change.name}` : change.kind;
118
119
  }
119
- function rewrites(changes, visible) {
120
+ function* rewrites(changes, visible) {
120
121
  const moves = new Map();
121
122
  const settled = new Set();
122
- for (const change of changes) {
123
+ const parts = new Map();
124
+ for (const [index, change] of changes.entries()) {
123
125
  if (change.kind === 'move')
124
126
  moves.set(change.path, change.to);
125
- 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') {
126
131
  settled.add(change.path);
127
132
  }
128
133
  if (change.kind === 'field')
129
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
+ }
130
142
  }
131
143
  if (moves.size === 0)
132
144
  return { kind: 'changes', changes };
133
145
  const merged = [...changes];
134
146
  const added = [];
135
- for (const [path, document] of documentsOf(visible.collection)) {
147
+ for (const [path, document] of yield* documentsOf(visible.collection)) {
136
148
  if (settled.has(path))
137
149
  continue;
138
150
  const moved = moves.get(path) ?? path;
139
151
  const definition = definitionOf(governing(visible.collection.composition, path), document.type);
140
- added.push(...repointed(document, definition, path, moved, moves, settled));
141
- 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);
142
156
  if (relinked.kind === 'refused')
143
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
+ }
144
164
  for (const change of relinked.changes) {
145
165
  if (change.kind === 'replace') {
146
- 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
+ }
147
171
  added.push(change);
148
172
  continue;
149
173
  }
150
- const index = merged.findIndex((item) => item.kind === change.kind &&
151
- item.path === path &&
152
- (change.kind !== 'section' || (item.kind === 'section' && item.name === change.name)));
153
- if (index === -1)
174
+ const index = keyed.get(partKey(change));
175
+ if (index === undefined)
154
176
  added.push(change);
155
177
  else
156
178
  merged[index] = change;
157
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);
158
183
  }
159
- return { kind: 'changes', changes: [...added, ...merged] };
184
+ return {
185
+ kind: 'changes',
186
+ changes: [...added, ...merged.filter((change) => change !== null)],
187
+ };
160
188
  }
161
- function anticipated(changes, preconditions, rewritten, visible) {
162
- 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));
163
192
  return {
164
193
  changes: rewritten,
165
- preconditions: [...preconditionsOf(derived, visible, []), ...preconditions],
194
+ preconditions: [...(yield* preconditionsOf(derived, visible, [])), ...preconditions],
166
195
  };
167
196
  }
168
- export function providerRewritten(batch, visible) {
169
- const rewritten = rewrites(batch.changes, visible);
197
+ export function* providerRewritten(batch, visible) {
198
+ const rewritten = yield* rewrites(batch.changes, visible);
170
199
  if (rewritten.kind === 'refused')
171
200
  return batch;
172
201
  return {
173
202
  ...batch,
174
- ...anticipated(batch.changes, batch.preconditions, rewritten.changes, visible),
203
+ ...(yield* anticipated(batch.changes, batch.preconditions, rewritten.changes, visible)),
175
204
  };
176
205
  }
177
- function incoming(collection, removed) {
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],
261
+ };
262
+ }
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) {
178
272
  const found = new Map();
179
273
  const note = (target, file) => {
180
274
  if (!removed.has(target))
@@ -183,7 +277,20 @@ function incoming(collection, removed) {
183
277
  referrers.add(file);
184
278
  found.set(target, referrers);
185
279
  };
186
- for (const [path, document] of documentsOf(collection)) {
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)) {
187
294
  const definition = definitionOf(governing(collection.composition, path), document.type);
188
295
  for (const { value, to } of definition === undefined ? [] : references(document, definition)) {
189
296
  if (to.includes(ATTACHMENT))
@@ -192,8 +299,9 @@ function incoming(collection, removed) {
192
299
  if (named !== null)
193
300
  note(named, path);
194
301
  }
302
+ yield ROW_WEIGHT + collection.text[path].length;
195
303
  }
196
- const [first] = [...found.keys()].sort();
304
+ const [first] = yield* sorting(found.keys());
197
305
  if (first === undefined)
198
306
  return null;
199
307
  const every = new Set();
@@ -206,19 +314,23 @@ function incoming(collection, removed) {
206
314
  code: 'referenced-missing-file',
207
315
  retryable: false,
208
316
  file: first,
209
- referrers: [...every].sort(),
317
+ referrers: yield* sorting(every),
210
318
  },
211
319
  };
212
320
  }
213
321
  function affected(change) {
214
322
  return change.kind === 'move' ? [change.path, change.to] : [change.path];
215
323
  }
216
- function guard(changes, input) {
217
- const existing = new Set([
218
- ...Object.keys(input.visible.documents),
219
- ...input.visible.binaryTargets,
220
- ]);
221
- 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()) {
222
334
  for (const path of affected(change)) {
223
335
  const normalized = normalize(path);
224
336
  if (normalized === null || normalized === '')
@@ -226,22 +338,41 @@ function guard(changes, input) {
226
338
  if (covered(input.providerOwnedPaths, normalized)) {
227
339
  return refuse('provider-owned-path', normalized);
228
340
  }
229
- const operation = change.kind === 'remove' ? 'delete' : 'write';
341
+ const operation = change.kind === 'remove' ? removing : 'write';
230
342
  if (!input.local && !covered(input.operations[operation], normalized))
231
343
  return refuse(`${operation}-grant`, normalized);
232
344
  }
233
- if (change.kind === 'create') {
234
- 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))) {
235
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
+ }
236
366
  existing.add(change.path);
237
367
  continue;
238
368
  }
239
369
  if (change.kind === 'move') {
240
370
  if (!existing.has(change.path))
241
371
  return refuse('path-absent', change.path);
242
- if (existing.has(change.to))
243
- return refuse('path-collision', change.to);
244
- if (!input.local && 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)) {
245
376
  return refuse('unknown-revision', change.path);
246
377
  }
247
378
  existing.delete(change.path);
@@ -251,14 +382,14 @@ function guard(changes, input) {
251
382
  if (!existing.has(change.path))
252
383
  return refuse('path-absent', change.path);
253
384
  if (change.kind === 'remove') {
254
- if (!input.local && unrevisionedDocument(input.visible, change.path)) {
385
+ if (unrevised(input, change.path)) {
255
386
  return refuse('unknown-revision', change.path);
256
387
  }
257
388
  existing.delete(change.path);
258
389
  continue;
259
390
  }
260
391
  if (change.kind === 'replace') {
261
- if (!input.local && input.visible.revisions[change.path] === undefined) {
392
+ if (unrevised(input, change.path)) {
262
393
  return refuse('unknown-revision', change.path);
263
394
  }
264
395
  continue;
@@ -269,8 +400,28 @@ function guard(changes, input) {
269
400
  }
270
401
  return null;
271
402
  }
272
- function unrevisionedDocument(visible, path) {
273
- 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}`;
274
425
  }
275
426
  function existence(visible, path) {
276
427
  const revision = visible.revisions[path];
@@ -285,18 +436,18 @@ function sameTarget(precondition, change) {
285
436
  return ((precondition.kind !== 'field' && precondition.kind !== 'section') ||
286
437
  ((change.kind === 'field' || change.kind === 'section') && precondition.name === change.name));
287
438
  }
288
- function preconditionsOf(changes, visible, bases) {
439
+ function* preconditionsOf(changes, visible, bases) {
289
440
  const preconditions = [];
290
441
  const seen = new Set();
291
442
  const add = (precondition) => {
292
- const part = precondition.kind === 'field' || precondition.kind === 'section' ? precondition.name : '';
293
- const identity = `${precondition.kind} ${precondition.path} ${part}`;
294
- if (seen.has(identity))
443
+ const held = identity(precondition);
444
+ if (seen.has(held))
295
445
  return;
296
- seen.add(identity);
446
+ seen.add(held);
297
447
  preconditions.push(precondition);
298
448
  };
299
449
  for (const change of changes) {
450
+ yield ROW_WEIGHT;
300
451
  if (change.kind === 'field' || change.kind === 'section' || change.kind === 'preamble') {
301
452
  const based = bases.find((base) => sameTarget(base, change));
302
453
  if (based !== undefined) {
@@ -328,6 +479,13 @@ function preconditionsOf(changes, visible, bases) {
328
479
  add({ kind: 'absent', path: change.path });
329
480
  continue;
330
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
+ }
331
489
  add(existence(visible, change.path));
332
490
  if (change.kind === 'move')
333
491
  add({ kind: 'absent', path: change.to });
@@ -337,10 +495,13 @@ function preconditionsOf(changes, visible, bases) {
337
495
  function depth(path) {
338
496
  return path.split('/').length;
339
497
  }
340
- function exceeded(limits, changes, resulting) {
341
- 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));
342
500
  const created = changes
343
- .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')
344
505
  .map((change) => (change.kind === 'move' ? change.to : change.path));
345
506
  for (const limit of limits) {
346
507
  const value = limit.value;
@@ -354,9 +515,13 @@ function exceeded(limits, changes, resulting) {
354
515
  return refuse('provider-limit', undefined, limit.name);
355
516
  }
356
517
  if (limit.name === 'document-size') {
357
- const large = written.find((path) => (resulting.documents[path] ?? '').length > value);
358
- if (large !== undefined)
359
- 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
+ }
360
525
  }
361
526
  if (limit.name === 'path-length') {
362
527
  const long = created.find((path) => path.length > value);
@@ -386,7 +551,7 @@ function relativeField(path, name, value, handle, definition) {
386
551
  !opened.ok ||
387
552
  !JSON.stringify(value ?? null).includes(`/rooms/${handle}/`))
388
553
  return value;
389
- const held = apply(opened.document, { kind: 'field', name, value, prior: undefined });
554
+ const held = apply(opened.document, [{ kind: 'field', name, value, prior: undefined }]);
390
555
  if (held.outcome !== 'applied')
391
556
  return value;
392
557
  const result = relativize(held.document, path, handle, definition);
@@ -422,54 +587,356 @@ function relativeChanges(input) {
422
587
  return { ...change, fields, sections };
423
588
  });
424
589
  }
425
- function unmodeledType(collection, composition) {
426
- for (const [path, document] of documentsOf(collection)) {
427
- if (document.type !== undefined &&
428
- document.type !== 'any' &&
429
- governing(composition, path).status === 'unavailable')
430
- return true;
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 });
431
622
  }
432
- return false;
623
+ const applied = apply(document, [...parts.values()]);
624
+ return applied.outcome === 'applied' ? serialize(applied.document) : null;
433
625
  }
434
- export function buildBatch(input) {
435
- const changes = relativeChanges(input);
436
- const structural = changes.find((change) => change.kind === 'move' || change.kind === 'remove');
437
- if (structural !== undefined && unmodeledType(input.visible.collection, input.composition))
438
- return refuse('model-unavailable', structural.path);
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]);
439
767
  if (changes.length === 0)
440
- return refuse('no-change');
441
- const rewritten = rewrites(changes, input.visible);
442
- if (rewritten.kind === 'refused')
443
- return rewritten;
444
- const refusal = guard(changes, input);
445
- if (refusal !== null)
446
- return refusal;
447
- const preconditions = preconditionsOf(changes, input.visible, input.bases);
448
- const predicted = anticipated(changes, preconditions, rewritten.changes, input.visible);
449
- const resulting = overlay(input.visible, [
450
- { id: '', baseRoomVersion: input.baseRoomVersion, ...predicted },
451
- ]);
452
- 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) {
453
920
  if (removed.size > 0) {
454
- const referenced = incoming(collectionOf(resulting.documents, input.composition), removed);
921
+ const referenced = await stepped(incoming(collectionOf(resulting.documents, composition), removed, null));
455
922
  if (referenced !== null)
456
923
  return referenced;
457
924
  }
458
- const typed = (content) => {
459
- if (content === undefined)
460
- return false;
461
- const parsed = parse(content);
462
- return parsed.ok && parsed.document.type !== undefined && parsed.document.type !== 'any';
463
- };
464
- const typedPaths = [...new Set(predicted.changes.flatMap(affected))].filter((path) => typed(input.visible.documents[path]) || typed(resulting.documents[path]));
465
- const needsModel = typedPaths.length > 0;
466
- 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');
467
935
  if (withoutModel !== undefined)
468
936
  return refuse('model-unavailable', withoutModel);
469
- const validationText = needsModel
470
- ? resulting.documents
471
- : Object.fromEntries(Object.entries(resulting.documents).filter(([path]) => predicted.changes.some((change) => affected(change).includes(path))));
472
- 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);
473
940
  if (verdict.verdict === 'refuse') {
474
941
  return {
475
942
  kind: 'refused',
@@ -482,12 +949,92 @@ export function buildBatch(input) {
482
949
  },
483
950
  };
484
951
  }
485
- 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));
486
1036
  if (limited !== null)
487
1037
  return limited;
488
- const validated = typedPaths
489
- .map((path) => governing(input.composition, path))
490
- .find((model) => model.status !== 'unavailable');
491
1038
  return {
492
1039
  kind: 'batch',
493
1040
  batch: immutable({
@@ -496,15 +1043,126 @@ export function buildBatch(input) {
496
1043
  changes,
497
1044
  preconditions,
498
1045
  ...(input.writtenBy === undefined ? {} : { writtenBy: input.writtenBy }),
499
- ...(validated === undefined
500
- ? {}
501
- : {
502
- validation: {
503
- identity: validated.snapshot.identity,
504
- version: validated.snapshot.version,
505
- source: validated.status,
506
- },
507
- }),
1046
+ ...validationOf(judged.validated),
508
1047
  }),
509
1048
  };
510
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
+ }