@agentshouse/mdruntime 0.1.0-cbb8d8acc9d19 → 0.1.0-cc56db96f86ae

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