@agentshouse/mdruntime 0.1.0-cf6041a06849c → 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 +23 -10
  2. package/dist/adapter.d.ts +144 -29
  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 +53 -6
  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 +41 -6
  14. package/dist/bootstrap.d.ts.map +1 -1
  15. package/dist/bootstrap.js +224 -45
  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 -56
  22. package/dist/change.d.ts +45 -0
  23. package/dist/change.d.ts.map +1 -1
  24. package/dist/change.js +34 -0
  25. package/dist/client.d.ts +35 -14
  26. package/dist/client.d.ts.map +1 -1
  27. package/dist/client.js +156 -19
  28. package/dist/collection.d.ts +14 -13
  29. package/dist/collection.d.ts.map +1 -1
  30. package/dist/collection.js +66 -54
  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 +63 -20
  35. package/dist/connection.d.ts.map +1 -1
  36. package/dist/connection.js +3889 -743
  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 -2
  65. package/dist/host.d.ts.map +1 -1
  66. package/dist/host.js +134 -21
  67. package/dist/identity.d.ts +4 -0
  68. package/dist/identity.d.ts.map +1 -1
  69. package/dist/identity.js +16 -1
  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 +13 -5
  80. package/dist/memory.d.ts.map +1 -1
  81. package/dist/memory.js +140 -31
  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 +20 -4
  86. package/dist/model.d.ts.map +1 -1
  87. package/dist/model.js +19 -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 -1
  107. package/dist/paths.d.ts.map +1 -1
  108. package/dist/paths.js +4 -16
  109. package/dist/query.d.ts +71 -5
  110. package/dist/query.d.ts.map +1 -1
  111. package/dist/query.js +100 -26
  112. package/dist/reach.d.ts +18 -4
  113. package/dist/reach.d.ts.map +1 -1
  114. package/dist/reach.js +247 -84
  115. package/dist/react.d.ts +31 -19
  116. package/dist/react.d.ts.map +1 -1
  117. package/dist/react.js +13 -21
  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 +19 -8
  128. package/dist/refresh.d.ts.map +1 -1
  129. package/dist/refresh.js +125 -57
  130. package/dist/replica.d.ts +59 -33
  131. package/dist/replica.d.ts.map +1 -1
  132. package/dist/replica.js +322 -155
  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 +186 -37
  136. package/dist/rows.d.ts +39 -34
  137. package/dist/rows.d.ts.map +1 -1
  138. package/dist/rows.js +197 -80
  139. package/dist/save.d.ts +74 -4
  140. package/dist/save.d.ts.map +1 -1
  141. package/dist/save.js +900 -178
  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 +132 -58
  155. package/dist/store.d.ts.map +1 -1
  156. package/dist/store.js +379 -344
  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 +173 -33
  173. package/dist/wire.d.ts.map +1 -1
  174. package/dist/wire.js +69 -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 -231
  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 -377
package/dist/save.js CHANGED
@@ -1,52 +1,73 @@
1
- import { definitionOf, referenceField } from './model.js';
2
- import { apply, links, parse, retarget, serialize, target, } from '@agentshouse/mdmodel';
3
- import { immutable, } from './change.js';
4
- import { governing, parseCollection, validateCollection, } from './collection.js';
1
+ import { definitionOf } from './model.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, relative } 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
  },
16
20
  };
17
21
  }
18
- function repointed(value, from, to, moves) {
19
- if (Array.isArray(value)) {
20
- const next = value.map((item) => repointed(item, from, to, moves) ?? item);
21
- return next.some((item, index) => item !== value[index]) ? next : undefined;
22
- }
23
- if (typeof value !== 'string')
24
- return undefined;
25
- const named = target(from, value);
26
- if (named === null)
27
- return undefined;
28
- const destination = moves.get(named) ?? named;
29
- return target(to, value) === destination ? undefined : retarget(value, relative(to, destination));
30
- }
31
- function edited(document, path, changes) {
32
- let next = document;
33
- for (const change of changes) {
34
- if (change.path !== path)
22
+ function repointed(document, definition, from, to, moves, settled) {
23
+ const parsed = parse(document.content, from);
24
+ if (!parsed.ok)
25
+ return [];
26
+ const changes = [];
27
+ const names = new Set();
28
+ for (const { pointer, value } of definition === undefined
29
+ ? []
30
+ : references(document, definition)) {
31
+ const [name = ''] = pointerSegments(pointer);
32
+ const named = target(from, value);
33
+ if (named === null || settled.has(`${from} ${name}`))
35
34
  continue;
36
- if (change.kind !== 'section' && change.kind !== 'preamble')
35
+ const destination = moves.get(named) ?? named;
36
+ if (target(to, value) === destination)
37
37
  continue;
38
- const applied = apply(next, change.kind === 'preamble'
39
- ? { kind: 'preamble', value: change.value, prior: next.preamble }
40
- : {
38
+ changes.push({
39
+ kind: 'reference',
40
+ pointer,
41
+ value: retarget(value, relative(to, destination)),
42
+ prior: value,
43
+ });
44
+ names.add(name);
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;
52
+ return [...names].map((name) => ({ kind: 'field', path: from, name, value: next.fields[name] }));
53
+ }
54
+ function edited(document, changes) {
55
+ const parts = new Map();
56
+ for (const change of changes) {
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}`, {
41
61
  kind: 'section',
42
62
  name: change.name,
43
63
  value: change.value,
44
- prior: next.sections[change.name],
64
+ prior: document.sections[change.name] ?? '',
45
65
  });
46
- if (applied.outcome === 'applied')
47
- next = applied.document;
48
66
  }
49
- 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;
50
71
  }
51
72
  function bodies(path, before, after) {
52
73
  const changes = [];
@@ -61,7 +82,7 @@ function bodies(path, before, after) {
61
82
  return changes;
62
83
  }
63
84
  function relinks(path, moved, before, moves) {
64
- let after = before;
85
+ const changes = [];
65
86
  for (const link of links(before, path)) {
66
87
  const destination = moves.get(link.target) ?? link.target;
67
88
  if (destination === link.target && moved === path)
@@ -69,19 +90,23 @@ function relinks(path, moved, before, moves) {
69
90
  const to = relative(moved, destination);
70
91
  if (to === relative(path, link.target))
71
92
  continue;
72
- const applied = apply(after, { kind: 'link', href: link.href, to });
73
- if (applied.outcome === 'refuse') {
74
- return {
75
- kind: 'refused',
76
- refusal: {
77
- rule: applied.refusal.rule,
78
- file: path,
79
- ...(applied.refusal.location === undefined ? {} : { location: applied.refusal.location }),
80
- },
81
- };
82
- }
83
- after = applied.document;
93
+ changes.push({ kind: 'link', href: link.href, to });
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
+ };
84
108
  }
109
+ const after = applied.document;
85
110
  if (after === before)
86
111
  return { kind: 'changes', changes: [] };
87
112
  if (before.type === undefined) {
@@ -89,93 +114,161 @@ function relinks(path, moved, before, moves) {
89
114
  }
90
115
  return { kind: 'changes', changes: bodies(path, before, after) };
91
116
  }
92
- function subsumed(merged, path) {
93
- for (let index = merged.length - 1; index >= 0; index -= 1) {
94
- const item = merged[index];
95
- if (item.path !== path)
96
- continue;
97
- if (item.kind === 'section' || item.kind === 'preamble')
98
- merged.splice(index, 1);
99
- }
117
+ function partKey(change) {
118
+ return change.kind === 'section' ? `section ${change.name}` : change.kind;
100
119
  }
101
- function rewrites(changes, visible) {
120
+ function* rewrites(changes, visible) {
102
121
  const moves = new Map();
103
122
  const settled = new Set();
104
- for (const change of changes) {
123
+ const parts = new Map();
124
+ for (const [index, change] of changes.entries()) {
105
125
  if (change.kind === 'move')
106
126
  moves.set(change.path, change.to);
107
- 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') {
108
131
  settled.add(change.path);
109
132
  }
110
133
  if (change.kind === 'field')
111
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
+ }
112
142
  }
113
143
  if (moves.size === 0)
114
144
  return { kind: 'changes', changes };
115
145
  const merged = [...changes];
116
146
  const added = [];
117
- for (const [path, document] of Object.entries(visible.collection.documents)) {
147
+ for (const [path, document] of yield* documentsOf(visible.collection)) {
118
148
  if (settled.has(path))
119
149
  continue;
120
150
  const moved = moves.get(path) ?? path;
121
151
  const definition = definitionOf(governing(visible.collection.composition, path), document.type);
122
- for (const [name, spec] of Object.entries(definition?.fields ?? {})) {
123
- if (!referenceField(spec) || settled.has(`${path} ${name}`))
124
- continue;
125
- const value = repointed(document.fields[name], path, moved, moves);
126
- if (value === undefined)
127
- continue;
128
- added.push({ kind: 'field', path, name, value });
129
- }
130
- 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);
131
156
  if (relinked.kind === 'refused')
132
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
+ }
133
164
  for (const change of relinked.changes) {
134
165
  if (change.kind === 'replace') {
135
- 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
+ }
136
171
  added.push(change);
137
172
  continue;
138
173
  }
139
- const index = merged.findIndex((item) => item.kind === change.kind &&
140
- item.path === path &&
141
- (change.kind !== 'section' || (item.kind === 'section' && item.name === change.name)));
142
- if (index === -1)
174
+ const index = keyed.get(partKey(change));
175
+ if (index === undefined)
143
176
  added.push(change);
144
177
  else
145
178
  merged[index] = change;
146
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);
147
183
  }
148
- return { kind: 'changes', changes: [...added, ...merged] };
184
+ return {
185
+ kind: 'changes',
186
+ changes: [...added, ...merged.filter((change) => change !== null)],
187
+ };
149
188
  }
150
- function anticipated(changes, preconditions, rewritten, visible) {
151
- 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));
152
192
  return {
153
193
  changes: rewritten,
154
- preconditions: [...preconditionsOf(derived, visible, []), ...preconditions],
194
+ preconditions: [...(yield* preconditionsOf(derived, visible, [])), ...preconditions],
155
195
  };
156
196
  }
157
- export function providerRewritten(batch, visible) {
158
- const rewritten = rewrites(batch.changes, visible);
197
+ export function* providerRewritten(batch, visible) {
198
+ const rewritten = yield* rewrites(batch.changes, visible);
159
199
  if (rewritten.kind === 'refused')
160
200
  return batch;
161
201
  return {
162
202
  ...batch,
163
- ...anticipated(batch.changes, batch.preconditions, rewritten.changes, visible),
203
+ ...(yield* anticipated(batch.changes, batch.preconditions, rewritten.changes, visible)),
164
204
  };
165
205
  }
166
- function targets(value, from) {
167
- const values = Array.isArray(value) ? value : [value];
168
- const resolved = [];
169
- for (const item of values) {
170
- if (typeof item !== 'string')
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))
171
250
  continue;
172
- const named = target(from, item);
173
- if (named !== null)
174
- resolved.push(named);
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;
175
254
  }
176
- return resolved;
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');
177
265
  }
178
- function incoming(collection, removed) {
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) {
179
272
  const found = new Map();
180
273
  const note = (target, file) => {
181
274
  if (!removed.has(target))
@@ -184,18 +277,31 @@ function incoming(collection, removed) {
184
277
  referrers.add(file);
185
278
  found.set(target, referrers);
186
279
  };
187
- for (const [path, document] of Object.entries(collection.documents)) {
280
+ const read = function* (paths) {
281
+ const seen = new Set();
282
+ for (const path of paths) {
283
+ if (seen.has(path))
284
+ continue;
285
+ seen.add(path);
286
+ const document = collection.document(path);
287
+ if (document !== undefined)
288
+ yield [path, document];
289
+ }
290
+ };
291
+ for (const [path, document] of candidates === null
292
+ ? yield* documentsOf(collection)
293
+ : read(candidates)) {
188
294
  const definition = definitionOf(governing(collection.composition, path), document.type);
189
- for (const [name, spec] of Object.entries(definition?.fields ?? {})) {
190
- if (!referenceField(spec))
295
+ for (const { value, to } of definition === undefined ? [] : references(document, definition)) {
296
+ if (to.includes(ATTACHMENT))
191
297
  continue;
192
- for (const named of targets(document.fields[name], path))
298
+ const named = target(path, value);
299
+ if (named !== null)
193
300
  note(named, path);
194
301
  }
195
- for (const link of links(document, path))
196
- note(link.target, path);
302
+ yield ROW_WEIGHT + collection.text[path].length;
197
303
  }
198
- const [first] = [...found.keys()].sort();
304
+ const [first] = yield* sorting(found.keys());
199
305
  if (first === undefined)
200
306
  return null;
201
307
  const every = new Set();
@@ -204,18 +310,27 @@ function incoming(collection, removed) {
204
310
  every.add(file);
205
311
  return {
206
312
  kind: 'refused',
207
- 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
+ },
208
319
  };
209
320
  }
210
321
  function affected(change) {
211
322
  return change.kind === 'move' ? [change.path, change.to] : [change.path];
212
323
  }
213
- function guard(changes, input) {
214
- const existing = new Set([
215
- ...Object.keys(input.visible.documents),
216
- ...input.visible.binaryTargets,
217
- ]);
218
- 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()) {
219
334
  for (const path of affected(change)) {
220
335
  const normalized = normalize(path);
221
336
  if (normalized === null || normalized === '')
@@ -223,22 +338,41 @@ function guard(changes, input) {
223
338
  if (covered(input.providerOwnedPaths, normalized)) {
224
339
  return refuse('provider-owned-path', normalized);
225
340
  }
226
- const operation = change.kind === 'remove' ? 'delete' : 'write';
227
- if (!covered(input.operations[operation], normalized))
341
+ const operation = change.kind === 'remove' ? removing : 'write';
342
+ if (!input.local && !covered(input.operations[operation], normalized))
228
343
  return refuse(`${operation}-grant`, normalized);
229
344
  }
230
- if (change.kind === 'create') {
231
- 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))) {
232
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
+ }
233
366
  existing.add(change.path);
234
367
  continue;
235
368
  }
236
369
  if (change.kind === 'move') {
237
370
  if (!existing.has(change.path))
238
371
  return refuse('path-absent', change.path);
239
- if (existing.has(change.to))
240
- return refuse('path-collision', change.to);
241
- 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)) {
242
376
  return refuse('unknown-revision', change.path);
243
377
  }
244
378
  existing.delete(change.path);
@@ -248,30 +382,53 @@ function guard(changes, input) {
248
382
  if (!existing.has(change.path))
249
383
  return refuse('path-absent', change.path);
250
384
  if (change.kind === 'remove') {
251
- if (unrevisionedDocument(input.visible, change.path)) {
385
+ if (unrevised(input, change.path)) {
252
386
  return refuse('unknown-revision', change.path);
253
387
  }
254
388
  existing.delete(change.path);
255
389
  continue;
256
390
  }
257
391
  if (change.kind === 'replace') {
258
- if (input.visible.revisions[change.path] === undefined) {
392
+ if (unrevised(input, change.path)) {
259
393
  return refuse('unknown-revision', change.path);
260
394
  }
261
395
  continue;
262
396
  }
263
- if (input.visible.collection.documents[change.path] === undefined) {
397
+ if (input.visible.collection.document(change.path) === undefined) {
264
398
  return refuse('path-absent', change.path);
265
399
  }
266
400
  }
267
401
  return null;
268
402
  }
269
- function unrevisionedDocument(visible, path) {
270
- 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}`;
271
425
  }
272
426
  function existence(visible, path) {
273
427
  const revision = visible.revisions[path];
274
- 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 };
275
432
  }
276
433
  function sameTarget(precondition, change) {
277
434
  if (precondition.kind !== change.kind || precondition.path !== change.path)
@@ -279,25 +436,25 @@ function sameTarget(precondition, change) {
279
436
  return ((precondition.kind !== 'field' && precondition.kind !== 'section') ||
280
437
  ((change.kind === 'field' || change.kind === 'section') && precondition.name === change.name));
281
438
  }
282
- function preconditionsOf(changes, visible, bases) {
439
+ function* preconditionsOf(changes, visible, bases) {
283
440
  const preconditions = [];
284
441
  const seen = new Set();
285
442
  const add = (precondition) => {
286
- const part = precondition.kind === 'field' || precondition.kind === 'section' ? precondition.name : '';
287
- const identity = `${precondition.kind} ${precondition.path} ${part}`;
288
- if (seen.has(identity))
443
+ const held = identity(precondition);
444
+ if (seen.has(held))
289
445
  return;
290
- seen.add(identity);
446
+ seen.add(held);
291
447
  preconditions.push(precondition);
292
448
  };
293
449
  for (const change of changes) {
450
+ yield ROW_WEIGHT;
294
451
  if (change.kind === 'field' || change.kind === 'section' || change.kind === 'preamble') {
295
452
  const based = bases.find((base) => sameTarget(base, change));
296
453
  if (based !== undefined) {
297
454
  add(based);
298
455
  continue;
299
456
  }
300
- const document = visible.collection.documents[change.path];
457
+ const document = visible.collection.document(change.path);
301
458
  if (change.kind === 'field') {
302
459
  add({
303
460
  kind: 'field',
@@ -322,6 +479,13 @@ function preconditionsOf(changes, visible, bases) {
322
479
  add({ kind: 'absent', path: change.path });
323
480
  continue;
324
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
+ }
325
489
  add(existence(visible, change.path));
326
490
  if (change.kind === 'move')
327
491
  add({ kind: 'absent', path: change.to });
@@ -331,10 +495,13 @@ function preconditionsOf(changes, visible, bases) {
331
495
  function depth(path) {
332
496
  return path.split('/').length;
333
497
  }
334
- function exceeded(limits, changes, resulting) {
335
- 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));
336
500
  const created = changes
337
- .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')
338
505
  .map((change) => (change.kind === 'move' ? change.to : change.path));
339
506
  for (const limit of limits) {
340
507
  const value = limit.value;
@@ -348,9 +515,13 @@ function exceeded(limits, changes, resulting) {
348
515
  return refuse('provider-limit', undefined, limit.name);
349
516
  }
350
517
  if (limit.name === 'document-size') {
351
- const large = written.find((path) => (resulting.documents[path] ?? '').length > value);
352
- if (large !== undefined)
353
- 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
+ }
354
525
  }
355
526
  if (limit.name === 'path-length') {
356
527
  const long = created.find((path) => path.length > value);
@@ -365,65 +536,505 @@ function exceeded(limits, changes, resulting) {
365
536
  }
366
537
  return null;
367
538
  }
368
- export function buildBatch(input) {
369
- const structural = input.changes.find((change) => change.kind === 'move' || change.kind === 'remove');
370
- if (structural !== undefined &&
371
- Object.entries(input.visible.collection.documents).some(([path, document]) => document.type !== undefined &&
372
- document.type !== 'any' &&
373
- governing(input.composition, path).status === 'unavailable'))
374
- return refuse('model-unavailable', structural.path);
375
- if (input.changes.length === 0)
376
- return refuse('no-change');
377
- const rewritten = rewrites(input.changes, input.visible);
378
- if (rewritten.kind === 'refused')
379
- return rewritten;
380
- const changes = input.changes;
381
- const refusal = guard(changes, input);
382
- if (refusal !== null)
383
- return refusal;
384
- const preconditions = preconditionsOf(changes, input.visible, input.bases);
385
- const predicted = anticipated(changes, preconditions, rewritten.changes, input.visible);
386
- const resulting = overlay(input.visible, [
387
- { id: '', baseRoomVersion: input.baseRoomVersion, ...predicted },
388
- ]);
389
- const removed = new Set(changes.filter((change) => change.kind === 'remove').map((change) => change.path));
539
+ function shellRelative(text, path, handle, definition) {
540
+ if (!text.includes(`/rooms/${handle}/`))
541
+ return null;
542
+ const parsed = parse(text, path);
543
+ if (!parsed.ok)
544
+ return null;
545
+ const result = relativize(parsed.document, path, handle, definition(parsed.document.type));
546
+ return result.outcome === 'applied' && result.document.content !== text ? result.document : null;
547
+ }
548
+ function relativeField(path, name, value, handle, definition) {
549
+ const opened = parse('---\n---\n');
550
+ if (definition === undefined ||
551
+ !opened.ok ||
552
+ !JSON.stringify(value ?? null).includes(`/rooms/${handle}/`))
553
+ return value;
554
+ const held = apply(opened.document, [{ kind: 'field', name, value, prior: undefined }]);
555
+ if (held.outcome !== 'applied')
556
+ return value;
557
+ const result = relativize(held.document, path, handle, definition);
558
+ return result.outcome === 'applied' ? result.document.fields[name] : value;
559
+ }
560
+ function relativeChanges(input) {
561
+ const { handle, composition, visible } = input;
562
+ const typed = (path) => (type) => definitionOf(governing(composition, path), type);
563
+ const body = (path, text) => shellRelative(text, path, handle, () => undefined)?.content ?? text;
564
+ return input.changes.map((change) => {
565
+ const definition = typed(change.path);
566
+ if (change.kind === 'field') {
567
+ const type = visible.collection.document(change.path)?.type;
568
+ const value = relativeField(change.path, change.name, change.value, handle, definition(type));
569
+ return value === change.value ? change : { ...change, value };
570
+ }
571
+ if (change.kind === 'section' || change.kind === 'preamble') {
572
+ const value = body(change.path, change.value);
573
+ return value === change.value ? change : { ...change, value };
574
+ }
575
+ if (change.kind === 'replace' || (change.kind === 'create' && 'content' in change)) {
576
+ const content = shellRelative(change.content, change.path, handle, definition)?.content ?? change.content;
577
+ return content === change.content ? change : { ...change, content };
578
+ }
579
+ if (change.kind !== 'create')
580
+ return change;
581
+ const type = typeof change.fields.type === 'string' ? change.fields.type : undefined;
582
+ const fields = Object.fromEntries(Object.entries(change.fields).map(([name, value]) => [
583
+ name,
584
+ relativeField(change.path, name, value, handle, definition(type)),
585
+ ]));
586
+ const sections = Object.fromEntries(Object.entries(change.sections).map(([name, value]) => [name, body(change.path, value)]));
587
+ return { ...change, fields, sections };
588
+ });
589
+ }
590
+ function unqueueable(file) {
591
+ return { kind: 'refused', refusal: { code: 'unknown-revision', retryable: false, file } };
592
+ }
593
+ function isPart(change) {
594
+ return change.kind === 'field' || change.kind === 'section' || change.kind === 'preamble';
595
+ }
596
+ function touched(change) {
597
+ return change.kind === 'move' ? [change.path, change.to] : [change.path];
598
+ }
599
+ function partsApplied(content, path, changes) {
600
+ const parsed = parse(content, path);
601
+ if (!parsed.ok)
602
+ return null;
603
+ const document = parsed.document;
604
+ const parts = new Map();
605
+ for (const change of changes) {
606
+ if (change.kind === 'field')
607
+ parts.set(`field ${change.name}`, {
608
+ kind: 'field',
609
+ name: change.name,
610
+ value: change.value,
611
+ prior: document.fields[change.name],
612
+ });
613
+ else if (change.kind === 'section')
614
+ parts.set(`section ${change.name}`, {
615
+ kind: 'section',
616
+ name: change.name,
617
+ value: change.value,
618
+ prior: document.sections[change.name] ?? '',
619
+ });
620
+ else
621
+ parts.set('preamble', { kind: 'preamble', value: change.value, prior: document.preamble });
622
+ }
623
+ const applied = apply(document, [...parts.values()]);
624
+ return applied.outcome === 'applied' ? serialize(applied.document) : null;
625
+ }
626
+ function settledParts(changes, deferred) {
627
+ for (const [held, parts] of deferred) {
628
+ const content = partsApplied(held.content, held.path, parts);
629
+ if (content === null)
630
+ return held.path;
631
+ changes[changes.indexOf(held)] = { ...held, content };
632
+ }
633
+ deferred.clear();
634
+ return null;
635
+ }
636
+ function folded(changes, change, appending, deferred) {
637
+ const held = changes.find((item) => item.path === change.path && (item.kind === 'create' || item.kind === 'replace'));
638
+ const whole = held === undefined ? -1 : changes.indexOf(held);
639
+ const occupied = changes.some((item) => !isPart(item) && item !== held && touched(item).includes(change.path));
640
+ if (isPart(change)) {
641
+ const same = changes.findIndex((item) => item.kind === change.kind &&
642
+ item.path === change.path &&
643
+ (change.kind === 'preamble' || ('name' in item && item.name === change.name)));
644
+ if (same !== -1) {
645
+ changes[same] = change;
646
+ return 'folded';
647
+ }
648
+ if (held === undefined) {
649
+ if (!appending)
650
+ return 'skipped';
651
+ if (occupied)
652
+ return 'refused';
653
+ changes.push(change);
654
+ return 'appended';
655
+ }
656
+ if ('content' in held) {
657
+ const parts = deferred.get(held);
658
+ if (parts === undefined)
659
+ deferred.set(held, [change]);
660
+ else
661
+ parts.push(change);
662
+ return 'folded';
663
+ }
664
+ if (change.kind === 'preamble')
665
+ return 'refused';
666
+ changes[whole] =
667
+ change.kind === 'field'
668
+ ? { ...held, fields: { ...held.fields, [change.name]: change.value } }
669
+ : { ...held, sections: { ...held.sections, [change.name]: change.value } };
670
+ return 'folded';
671
+ }
672
+ if (change.kind === 'replace') {
673
+ if (held !== undefined) {
674
+ if (!('content' in held))
675
+ return 'refused';
676
+ changes[whole] = { ...held, content: change.content };
677
+ return 'folded';
678
+ }
679
+ const parts = changes.filter((item) => isPart(item) && item.path === change.path);
680
+ if (parts.length === 0 && !appending)
681
+ return 'skipped';
682
+ if (occupied)
683
+ return 'refused';
684
+ changes.splice(0, changes.length, ...changes.filter((item) => !parts.includes(item)), change);
685
+ return 'appended';
686
+ }
687
+ if (changes.some((item) => touched(change).some((path) => touched(item).includes(path))))
688
+ return 'refused';
689
+ changes.push(change);
690
+ return 'appended';
691
+ }
692
+ function* relinked(batch, move, before, after, appending) {
693
+ const derived = yield* rewrites([move], after);
694
+ if (derived.kind === 'refused')
695
+ return derived;
696
+ const changes = [...batch.changes];
697
+ const appended = [];
698
+ let changed = false;
699
+ const deferred = new Map();
700
+ for (const change of derived.changes) {
701
+ if (change === move)
702
+ continue;
703
+ const unsettled = isPart(change) ? null : settledParts(changes, deferred);
704
+ if (unsettled !== null)
705
+ return unqueueable(unsettled);
706
+ const outcome = folded(changes, change, appending, deferred);
707
+ if (outcome === 'refused')
708
+ return unqueueable(change.path);
709
+ if (outcome === 'appended')
710
+ appended.push(change);
711
+ if (outcome !== 'skipped')
712
+ changed = true;
713
+ }
714
+ const unsettled = settledParts(changes, deferred);
715
+ if (unsettled !== null)
716
+ return unqueueable(unsettled);
717
+ if (!changed)
718
+ return { kind: 'relinked', batch };
719
+ const held = new Set(batch.preconditions.map(identity));
720
+ const added = yield* preconditionsOf(appended, before, []);
721
+ return {
722
+ kind: 'relinked',
723
+ batch: immutable({
724
+ ...batch,
725
+ id: changeBatchIdentity(),
726
+ changes,
727
+ preconditions: [
728
+ ...batch.preconditions,
729
+ ...added.filter((precondition) => !held.has(identity(precondition))),
730
+ ],
731
+ }),
732
+ };
733
+ }
734
+ function leading(changes, at) {
735
+ const from = departure(changes, at);
736
+ return new Set(changes.filter((change, index) => index < at && change.kind === 'move' && departure(changes, index) === from));
737
+ }
738
+ function created(batch, at) {
739
+ const file = batch.changes[at];
740
+ return (leading(batch.changes, at).size === 0 &&
741
+ batch.preconditions.some((precondition) => precondition.kind === 'absent' && precondition.path === file.path));
742
+ }
743
+ function withdrawnFile(batch, change, at) {
744
+ const file = batch.changes[at];
745
+ const from = departure(batch.changes, at);
746
+ const moves = leading(batch.changes, at);
747
+ const fresh = created(batch, at);
748
+ const vacated = new Set([...moves].map((move) => move.to));
749
+ if (fresh)
750
+ vacated.add(file.path);
751
+ const replaced = fresh
752
+ ? change.kind === 'remove'
753
+ ? []
754
+ : [{ ...file, path: change.to }]
755
+ : change.kind === 'remove'
756
+ ? [{ kind: 'remove', path: from }]
757
+ : change.to === from
758
+ ? [{ ...file, path: from }]
759
+ : [
760
+ { kind: 'move', path: from, to: change.to },
761
+ { ...file, path: change.to },
762
+ ];
763
+ const added = change.kind === 'move' && (fresh || change.to !== from)
764
+ ? [{ kind: 'absent', path: change.to }]
765
+ : [];
766
+ const changes = batch.changes.flatMap((item) => item === file ? replaced : item.kind === 'move' && moves.has(item) ? [] : [item]);
767
+ if (changes.length === 0)
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) {
390
920
  if (removed.size > 0) {
391
- const referenced = incoming(parseCollection(resulting.documents, input.composition), removed);
921
+ const referenced = await stepped(incoming(collectionOf(resulting.documents, composition), removed, null));
392
922
  if (referenced !== null)
393
923
  return referenced;
394
924
  }
395
- const typed = (content) => {
396
- if (content === undefined)
397
- return false;
398
- const parsed = parse(content);
399
- return parsed.ok && parsed.document.type !== undefined && parsed.document.type !== 'any';
400
- };
401
- const typedPaths = [...new Set(predicted.changes.flatMap(affected))].filter((path) => typed(input.visible.documents[path]) || typed(resulting.documents[path]));
402
- const needsModel = typedPaths.length > 0;
403
- 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');
404
935
  if (withoutModel !== undefined)
405
936
  return refuse('model-unavailable', withoutModel);
406
- const validationText = needsModel
407
- ? resulting.documents
408
- : Object.fromEntries(Object.entries(resulting.documents).filter(([path]) => predicted.changes.some((change) => affected(change).includes(path))));
409
- 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);
410
940
  if (verdict.verdict === 'refuse') {
411
941
  return {
412
942
  kind: 'refused',
413
943
  refusal: {
414
- rule: verdict.rule,
944
+ code: verdict.rule,
945
+ retryable: false,
415
946
  file: verdict.file,
416
947
  ...(verdict.location === undefined ? {} : { location: verdict.location }),
417
948
  ...(verdict.referrers === undefined ? {} : { referrers: verdict.referrers }),
418
949
  },
419
950
  };
420
951
  }
421
- 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));
422
1036
  if (limited !== null)
423
1037
  return limited;
424
- const validated = typedPaths
425
- .map((path) => governing(input.composition, path))
426
- .find((model) => model.status !== 'unavailable');
427
1038
  return {
428
1039
  kind: 'batch',
429
1040
  batch: immutable({
@@ -432,15 +1043,126 @@ export function buildBatch(input) {
432
1043
  changes,
433
1044
  preconditions,
434
1045
  ...(input.writtenBy === undefined ? {} : { writtenBy: input.writtenBy }),
435
- ...(validated === undefined
436
- ? {}
437
- : {
438
- validation: {
439
- identity: validated.snapshot.identity,
440
- version: validated.snapshot.version,
441
- source: validated.status,
442
- },
443
- }),
1046
+ ...validationOf(judged.validated),
444
1047
  }),
445
1048
  };
446
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
+ }