@agentshouse/mdruntime 0.1.0-cf6d5f0a5c9c8 → 0.1.0-cf6e25bcd52f6

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 (185) hide show
  1. package/README.md +21 -8
  2. package/dist/adapter.d.ts +139 -32
  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 +32 -9
  14. package/dist/bootstrap.d.ts.map +1 -1
  15. package/dist/bootstrap.js +192 -67
  16. package/dist/chain.d.ts +11 -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 -62
  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 +33 -14
  26. package/dist/client.d.ts.map +1 -1
  27. package/dist/client.js +123 -21
  28. package/dist/collection.d.ts +12 -6
  29. package/dist/collection.d.ts.map +1 -1
  30. package/dist/collection.js +51 -76
  31. package/dist/compile.d.ts +16 -0
  32. package/dist/compile.d.ts.map +1 -0
  33. package/dist/compile.js +765 -0
  34. package/dist/connection.d.ts +60 -20
  35. package/dist/connection.d.ts.map +1 -1
  36. package/dist/connection.js +3825 -793
  37. package/dist/conversation.d.ts +42 -12
  38. package/dist/conversation.d.ts.map +1 -1
  39. package/dist/conversation.js +642 -188
  40. package/dist/digest.d.ts +7 -0
  41. package/dist/digest.d.ts.map +1 -0
  42. package/dist/digest.js +113 -0
  43. package/dist/durability.d.ts +5 -1
  44. package/dist/durability.d.ts.map +1 -1
  45. package/dist/durability.js +5 -0
  46. package/dist/entry.d.ts +19 -0
  47. package/dist/entry.d.ts.map +1 -0
  48. package/dist/entry.js +151 -0
  49. package/dist/expectation.d.ts +21 -0
  50. package/dist/expectation.d.ts.map +1 -0
  51. package/dist/expectation.js +70 -0
  52. package/dist/failure.d.ts +13 -0
  53. package/dist/failure.d.ts.map +1 -0
  54. package/dist/failure.js +19 -0
  55. package/dist/fence.d.ts +6 -0
  56. package/dist/fence.d.ts.map +1 -0
  57. package/dist/fence.js +10 -0
  58. package/dist/file.d.ts +2 -19
  59. package/dist/file.d.ts.map +1 -1
  60. package/dist/file.js +7 -122
  61. package/dist/fulltext.d.ts +27 -0
  62. package/dist/fulltext.d.ts.map +1 -0
  63. package/dist/fulltext.js +368 -0
  64. package/dist/host.d.ts +15 -3
  65. package/dist/host.d.ts.map +1 -1
  66. package/dist/host.js +134 -22
  67. package/dist/identity.d.ts +3 -0
  68. package/dist/identity.d.ts.map +1 -1
  69. package/dist/identity.js +9 -0
  70. package/dist/index.d.ts +19 -9
  71. package/dist/index.d.ts.map +1 -1
  72. package/dist/index.js +8 -0
  73. package/dist/live.d.ts +22 -0
  74. package/dist/live.d.ts.map +1 -0
  75. package/dist/live.js +124 -0
  76. package/dist/media.d.ts +8 -0
  77. package/dist/media.d.ts.map +1 -0
  78. package/dist/media.js +208 -0
  79. package/dist/memory.d.ts +12 -5
  80. package/dist/memory.d.ts.map +1 -1
  81. package/dist/memory.js +128 -29
  82. package/dist/messages.d.ts +28 -0
  83. package/dist/messages.d.ts.map +1 -0
  84. package/dist/messages.js +90 -0
  85. package/dist/model.d.ts +13 -4
  86. package/dist/model.d.ts.map +1 -1
  87. package/dist/model.js +17 -2
  88. package/dist/name.d.ts +4 -0
  89. package/dist/name.d.ts.map +1 -0
  90. package/dist/name.js +122 -0
  91. package/dist/observation.d.ts +20 -2
  92. package/dist/observation.d.ts.map +1 -1
  93. package/dist/observation.js +113 -12
  94. package/dist/open.d.ts +2 -0
  95. package/dist/open.d.ts.map +1 -0
  96. package/dist/open.js +1 -0
  97. package/dist/opened.d.ts +22 -0
  98. package/dist/opened.d.ts.map +1 -0
  99. package/dist/opened.js +107 -0
  100. package/dist/outcome.d.ts +73 -3
  101. package/dist/outcome.d.ts.map +1 -1
  102. package/dist/outcome.js +81 -1
  103. package/dist/overlay.d.ts +15 -5
  104. package/dist/overlay.d.ts.map +1 -1
  105. package/dist/overlay.js +136 -180
  106. package/dist/paths.d.ts +1 -0
  107. package/dist/paths.d.ts.map +1 -1
  108. package/dist/paths.js +4 -0
  109. package/dist/query.d.ts +70 -4
  110. package/dist/query.d.ts.map +1 -1
  111. package/dist/query.js +51 -7
  112. package/dist/reach.d.ts +18 -4
  113. package/dist/reach.d.ts.map +1 -1
  114. package/dist/reach.js +246 -68
  115. package/dist/react.d.ts +12 -8
  116. package/dist/react.d.ts.map +1 -1
  117. package/dist/react.js +8 -17
  118. package/dist/readiness.d.ts +64 -0
  119. package/dist/readiness.d.ts.map +1 -0
  120. package/dist/readiness.js +33 -0
  121. package/dist/reads.d.ts +11 -0
  122. package/dist/reads.d.ts.map +1 -0
  123. package/dist/reads.js +100 -0
  124. package/dist/recovery.d.ts +25 -0
  125. package/dist/recovery.d.ts.map +1 -0
  126. package/dist/recovery.js +198 -0
  127. package/dist/refresh.d.ts +16 -15
  128. package/dist/refresh.d.ts.map +1 -1
  129. package/dist/refresh.js +113 -54
  130. package/dist/replica.d.ts +59 -33
  131. package/dist/replica.d.ts.map +1 -1
  132. package/dist/replica.js +320 -159
  133. package/dist/room-draft.d.ts +41 -9
  134. package/dist/room-draft.d.ts.map +1 -1
  135. package/dist/room-draft.js +185 -36
  136. package/dist/rows.d.ts +37 -35
  137. package/dist/rows.d.ts.map +1 -1
  138. package/dist/rows.js +173 -102
  139. package/dist/save.d.ts +73 -4
  140. package/dist/save.d.ts.map +1 -1
  141. package/dist/save.js +820 -151
  142. package/dist/search.d.ts +41 -29
  143. package/dist/search.d.ts.map +1 -1
  144. package/dist/search.js +37 -105
  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 +16 -0
  149. package/dist/shared.d.ts.map +1 -0
  150. package/dist/shared.js +431 -0
  151. package/dist/status.d.ts +11 -2
  152. package/dist/status.d.ts.map +1 -1
  153. package/dist/status.js +3 -9
  154. package/dist/store.d.ts +130 -80
  155. package/dist/store.d.ts.map +1 -1
  156. package/dist/store.js +376 -457
  157. package/dist/stored.d.ts +154 -0
  158. package/dist/stored.d.ts.map +1 -0
  159. package/dist/stored.js +578 -0
  160. package/dist/tables.d.ts +362 -0
  161. package/dist/tables.d.ts.map +1 -0
  162. package/dist/tables.js +627 -0
  163. package/dist/turn.d.ts +9 -0
  164. package/dist/turn.d.ts.map +1 -0
  165. package/dist/turn.js +75 -0
  166. package/dist/validation.d.ts +30 -0
  167. package/dist/validation.d.ts.map +1 -0
  168. package/dist/validation.js +162 -0
  169. package/dist/windows.d.ts +22 -0
  170. package/dist/windows.d.ts.map +1 -0
  171. package/dist/windows.js +89 -0
  172. package/dist/wire.d.ts +172 -33
  173. package/dist/wire.d.ts.map +1 -1
  174. package/dist/wire.js +68 -31
  175. package/dist/words.d.ts +19 -0
  176. package/dist/words.d.ts.map +1 -0
  177. package/dist/words.js +156 -0
  178. package/package.json +26 -15
  179. package/AGENTS.md +0 -253
  180. package/dist/attachment.d.ts +0 -3
  181. package/dist/attachment.d.ts.map +0 -1
  182. package/dist/attachment.js +0 -8
  183. package/dist/read-side.d.ts +0 -18
  184. package/dist/read-side.d.ts.map +0 -1
  185. 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 { 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';
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 documentsOf(visible.collection)) {
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 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)) {
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,14 +382,14 @@ 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;
@@ -262,12 +400,35 @@ function guard(changes, input) {
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,18 +436,18 @@ 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) {
@@ -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);
@@ -412,71 +587,454 @@ function relativeChanges(input) {
412
587
  return { ...change, fields, sections };
413
588
  });
414
589
  }
415
- function unmodeledType(collection, composition) {
416
- for (const [path, document] of documentsOf(collection)) {
417
- if (document.type !== undefined &&
418
- document.type !== 'any' &&
419
- governing(composition, path).status === 'unavailable')
420
- 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 });
421
622
  }
422
- return false;
623
+ const applied = apply(document, [...parts.values()]);
624
+ return applied.outcome === 'applied' ? serialize(applied.document) : null;
423
625
  }
424
- export function buildBatch(input) {
425
- const changes = relativeChanges(input);
426
- const structural = changes.find((change) => change.kind === 'move' || change.kind === 'remove');
427
- if (structural !== undefined && unmodeledType(input.visible.collection, input.composition))
428
- 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]);
429
767
  if (changes.length === 0)
430
- return refuse('no-change');
431
- const rewritten = rewrites(changes, input.visible);
432
- if (rewritten.kind === 'refused')
433
- return rewritten;
434
- const refusal = guard(changes, input);
435
- if (refusal !== null)
436
- return refusal;
437
- const preconditions = preconditionsOf(changes, input.visible, input.bases);
438
- const predicted = anticipated(changes, preconditions, rewritten.changes, input.visible);
439
- const resulting = overlay(input.visible, [
440
- { id: '', baseRoomVersion: input.baseRoomVersion, ...predicted },
441
- ]);
442
- 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) {
443
920
  if (removed.size > 0) {
444
- const referenced = incoming(collectionOf(resulting.documents, input.composition), removed);
921
+ const referenced = await stepped(incoming(collectionOf(resulting.documents, composition), removed, null));
445
922
  if (referenced !== null)
446
923
  return referenced;
447
924
  }
448
- const typed = (content) => {
449
- if (content === undefined)
450
- return false;
451
- const parsed = parse(content);
452
- return parsed.ok && parsed.document.type !== undefined && parsed.document.type !== 'any';
453
- };
454
- const typedPaths = [...new Set(predicted.changes.flatMap(affected))].filter((path) => typed(input.visible.documents[path]) || typed(resulting.documents[path]));
455
- const needsModel = typedPaths.length > 0;
456
- 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');
457
935
  if (withoutModel !== undefined)
458
936
  return refuse('model-unavailable', withoutModel);
459
- const validationText = needsModel
460
- ? resulting.documents
461
- : Object.fromEntries(Object.entries(resulting.documents).filter(([path]) => predicted.changes.some((change) => affected(change).includes(path))));
462
- 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);
463
940
  if (verdict.verdict === 'refuse') {
464
941
  return {
465
942
  kind: 'refused',
466
943
  refusal: {
467
- rule: verdict.rule,
944
+ code: verdict.rule,
945
+ retryable: false,
468
946
  file: verdict.file,
469
947
  ...(verdict.location === undefined ? {} : { location: verdict.location }),
470
948
  ...(verdict.referrers === undefined ? {} : { referrers: verdict.referrers }),
471
949
  },
472
950
  };
473
951
  }
474
- 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));
475
1036
  if (limited !== null)
476
1037
  return limited;
477
- const validated = typedPaths
478
- .map((path) => governing(input.composition, path))
479
- .find((model) => model.status !== 'unavailable');
480
1038
  return {
481
1039
  kind: 'batch',
482
1040
  batch: immutable({
@@ -485,15 +1043,126 @@ export function buildBatch(input) {
485
1043
  changes,
486
1044
  preconditions,
487
1045
  ...(input.writtenBy === undefined ? {} : { writtenBy: input.writtenBy }),
488
- ...(validated === undefined
489
- ? {}
490
- : {
491
- validation: {
492
- identity: validated.snapshot.identity,
493
- version: validated.snapshot.version,
494
- source: validated.status,
495
- },
496
- }),
1046
+ ...validationOf(judged.validated),
497
1047
  }),
498
1048
  };
499
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
+ }