@agentshouse/mdruntime 0.1.0-cbb8d8acc9d19 → 0.1.0-cbbae1fff01c6

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