@trioforge/crucible 0.0.0-stage → 0.1.0

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 (63) hide show
  1. package/LICENSE +21 -0
  2. package/client/package.json +4 -0
  3. package/emit/package.json +4 -0
  4. package/out/all.d.ts +7 -0
  5. package/out/all.luau +2 -0
  6. package/out/attribute.d.ts +7 -0
  7. package/out/attribute.luau +7 -0
  8. package/out/client/index.d.ts +3 -0
  9. package/out/client/init.luau +20 -0
  10. package/out/crucible-behaviour.d.ts +16 -0
  11. package/out/crucible-behaviour.luau +78 -0
  12. package/out/crucible-object.d.ts +10 -0
  13. package/out/crucible-object.luau +69 -0
  14. package/out/emit/index.d.ts +41 -0
  15. package/out/emit/init.luau +85 -0
  16. package/out/index.d.ts +12 -0
  17. package/out/init.luau +11 -0
  18. package/out/inject.d.ts +1 -0
  19. package/out/inject.luau +7 -0
  20. package/out/internal/binding.d.ts +1 -0
  21. package/out/internal/binding.luau +610 -0
  22. package/out/internal/boot.d.ts +1 -0
  23. package/out/internal/boot.luau +463 -0
  24. package/out/internal/construction-claim.d.ts +1 -0
  25. package/out/internal/construction-claim.luau +26 -0
  26. package/out/internal/dense-list.d.ts +1 -0
  27. package/out/internal/dense-list.luau +155 -0
  28. package/out/internal/diagnostics.d.ts +1 -0
  29. package/out/internal/diagnostics.luau +29 -0
  30. package/out/internal/hooks.d.ts +1 -0
  31. package/out/internal/hooks.luau +53 -0
  32. package/out/internal/implementers.d.ts +1 -0
  33. package/out/internal/implementers.luau +119 -0
  34. package/out/internal/lifecycle.d.ts +1 -0
  35. package/out/internal/lifecycle.luau +867 -0
  36. package/out/internal/log-output.d.ts +1 -0
  37. package/out/internal/log-output.luau +106 -0
  38. package/out/internal/port.d.ts +1 -0
  39. package/out/internal/port.luau +12 -0
  40. package/out/internal/registry.d.ts +1 -0
  41. package/out/internal/registry.luau +446 -0
  42. package/out/internal/roblox-port.d.ts +1 -0
  43. package/out/internal/roblox-port.luau +117 -0
  44. package/out/internal/runner.d.ts +1 -0
  45. package/out/internal/runner.luau +86 -0
  46. package/out/internal/scheduler.d.ts +1 -0
  47. package/out/internal/scheduler.luau +207 -0
  48. package/out/internal/state.d.ts +1 -0
  49. package/out/internal/state.luau +41 -0
  50. package/out/log-level.d.ts +7 -0
  51. package/out/log-level.luau +21 -0
  52. package/out/logging.d.ts +34 -0
  53. package/out/logging.luau +233 -0
  54. package/out/server/index.d.ts +2 -0
  55. package/out/server/init.luau +16 -0
  56. package/out/subsystem.d.ts +4 -0
  57. package/out/subsystem.luau +30 -0
  58. package/package.json +41 -5
  59. package/server/package.json +4 -0
  60. package/transformer/diagnostics.cjs +19 -0
  61. package/transformer/index.js +715 -0
  62. package/transformer/type-id.cjs +80 -0
  63. package/README.md +0 -3
@@ -0,0 +1,867 @@
1
+ -- Compiled with roblox-ts v3.0.0
2
+ local TS = _G[script]
3
+ local _runner = TS.import(script, script.Parent, "runner")
4
+ local call = _runner.call
5
+ local disown = _runner.disown
6
+ local own = _runner.own
7
+ local ownerOf = _runner.ownerOf
8
+ --[[
9
+ *
10
+ * @internal
11
+ * Pure transition function. The drain calls it until it returns undefined. It reads only the current state,
12
+ * so re-running it after a request converges on the same end state, and asking twice for the state an object
13
+ * already has finds no step.
14
+
15
+ ]]
16
+ local function nextStep(s)
17
+ local ending = s.fault ~= nil or s.want == 2
18
+ local _exp = s.phase
19
+ repeat
20
+ if _exp == 0 then
21
+ if ending then
22
+ return 10
23
+ end
24
+ return if s.gate >= 1 then 0 else nil
25
+ end
26
+ if _exp == 1 then
27
+ if ending then
28
+ return 10
29
+ end
30
+ return if s.gate >= 2 then 1 else nil
31
+ end
32
+ if _exp == 2 then
33
+ if ending then
34
+ return 9
35
+ end
36
+ if s.gate < 3 then
37
+ return nil
38
+ end
39
+ return if s.want == 0 then 2 else 3
40
+ end
41
+ if _exp == 3 then
42
+ if s.fault ~= nil or s.want ~= 0 then
43
+ return if s.joined or s.ticking then 7 else 8
44
+ end
45
+ if not s.joined then
46
+ return 4
47
+ end
48
+ if s.start == 0 then
49
+ return if s.gate >= 4 then 5 else nil
50
+ end
51
+ return if s.start == 2 and not s.ticking then 6 else nil
52
+ end
53
+ if _exp == 4 then
54
+ if ending then
55
+ return 9
56
+ end
57
+ return if s.want == 0 then 2 else nil
58
+ end
59
+ until true
60
+ return nil
61
+ end
62
+ local installed
63
+ local states = {}
64
+ --* @internal
65
+ local function useEffects(effects)
66
+ installed = effects
67
+ end
68
+ local function effects()
69
+ return installed or error("Crucible lifecycle used before boot")
70
+ end
71
+ --* @internal
72
+ local function stateOf(object)
73
+ local _object = object
74
+ return states[_object]
75
+ end
76
+ --* @internal Awake, not faulted, and not asked to go. `get` and queries see only live objects.
77
+ local function isLive(s)
78
+ return s.fault == nil and s.want ~= 2 and (s.phase == 2 or s.phase == 3 or s.phase == 4)
79
+ end
80
+ --* @internal Faulted or Destroyed.
81
+ local function hasEnded(s)
82
+ return s.phase == 5 or s.phase == 6
83
+ end
84
+ --* @internal A new generation, before its constructor runs. `admit` gives it a batch.
85
+ local function track(record, instance, attributes)
86
+ return {
87
+ work = 0,
88
+ phase = 0,
89
+ start = 0,
90
+ gate = 0,
91
+ want = 0,
92
+ joined = false,
93
+ ticking = false,
94
+ record = record,
95
+ instance = instance,
96
+ attributes = attributes,
97
+ queued = false,
98
+ transitions = 0,
99
+ }
100
+ end
101
+ --[[
102
+ *
103
+ * @internal
104
+ * The object whose code is running: the owner of a runner worker during a hook or callback, or of a start
105
+ * thread for its whole life. A thread made with `task.spawn` inside a hook has no owner.
106
+
107
+ ]]
108
+ local function current()
109
+ return ownerOf(coroutine.running())
110
+ end
111
+ local function note(s, hook, err)
112
+ if s.fault == nil then
113
+ s.fault = {
114
+ hook = hook,
115
+ error = err,
116
+ cleanup = {},
117
+ }
118
+ else
119
+ local _cleanup = s.fault.cleanup
120
+ local _arg0 = {
121
+ hook = hook,
122
+ error = err,
123
+ }
124
+ table.insert(_cleanup, _arg0)
125
+ end
126
+ end
127
+ --* A teardown failure attaches to the fault, or is diagnosed without faulting a clean generation.
128
+ local function cleanup(s, hook, err)
129
+ if s.fault ~= nil then
130
+ local _cleanup = s.fault.cleanup
131
+ local _arg0 = {
132
+ hook = hook,
133
+ error = err,
134
+ }
135
+ table.insert(_cleanup, _arg0)
136
+ else
137
+ effects().diagnose(s, hook, err)
138
+ end
139
+ end
140
+ local subsystemLacks = {
141
+ [2] = true,
142
+ [4] = true,
143
+ [5] = true,
144
+ }
145
+ local function run(s, hook)
146
+ local _condition = s.record.kind == 0
147
+ if _condition then
148
+ local _hook = hook
149
+ _condition = subsystemLacks[_hook] ~= nil
150
+ end
151
+ if _condition then
152
+ return nil
153
+ end
154
+ local caller = effects().hook(s, hook)
155
+ if caller == nil then
156
+ return nil
157
+ end
158
+ local result, value = call(caller, s, nil, s)
159
+ if result ~= 0 then
160
+ note(s, hook, value)
161
+ end
162
+ end
163
+ local function construct(s)
164
+ return effects().construct(s)
165
+ end
166
+ local function release(s)
167
+ if s.object ~= nil then
168
+ local _object = s.object
169
+ states[_object] = nil
170
+ end
171
+ local subscriptions = s.subscriptions
172
+ s.subscriptions = nil
173
+ if subscriptions == nil then
174
+ return nil
175
+ end
176
+ for _, disconnect in subscriptions do
177
+ local ok, err = pcall(disconnect)
178
+ if not ok then
179
+ cleanup(s, 11, err)
180
+ end
181
+ end
182
+ end
183
+ local function finish(s)
184
+ s.phase = if s.fault == nil then 6 else 5
185
+ release(s)
186
+ effects().ended(s)
187
+ end
188
+ local function traceback(err)
189
+ return debug.traceback(tostring(err), 2)
190
+ end
191
+ local finishStart
192
+ local function runStart(s)
193
+ local ok, err = xpcall(effects().start, traceback, s)
194
+ disown(coroutine.running())
195
+ finishStart(s, ok, err)
196
+ end
197
+ local function cancelSuspended(thread)
198
+ disown(thread)
199
+ return pcall(effects().cancel, thread)
200
+ end
201
+ --[[
202
+ *
203
+ * Only a suspended thread can be closed. A start thread that is running, or normal because it resumed the
204
+ * thread now draining, keeps its owner, so `current()` sees an ended object and `observe` subscribes nothing.
205
+ * A deferred check cancels it at its next yield, and `finishStart` ignores it if it returns first.
206
+
207
+ ]]
208
+ local function stopStart(s, thread)
209
+ if coroutine.status(thread) == "suspended" then
210
+ local ok, err = cancelSuspended(thread)
211
+ if not ok then
212
+ cleanup(s, 10, err)
213
+ end
214
+ return nil
215
+ end
216
+ local fx = effects()
217
+ fx.defer(function()
218
+ if coroutine.status(thread) ~= "suspended" then
219
+ return nil
220
+ end
221
+ local ok, err = cancelSuspended(thread)
222
+ if not ok then
223
+ fx.diagnose(s, 10, err)
224
+ end
225
+ end)
226
+ end
227
+ local function perform(s, step)
228
+ local fx = effects()
229
+ repeat
230
+ if step == 0 then
231
+ local result, value = call(construct, s, nil, s)
232
+ if result ~= 0 then
233
+ note(s, 0, value)
234
+ else
235
+ local object = value
236
+ s.object = object
237
+ local _s = s
238
+ states[object] = _s
239
+ s.phase = 1
240
+ end
241
+ return nil
242
+ end
243
+ if step == 1 then
244
+ s.phase = 2
245
+ run(s, 1)
246
+ return nil
247
+ end
248
+ if step == 2 then
249
+ s.phase = 3
250
+ run(s, 2)
251
+ return nil
252
+ end
253
+ if step == 3 then
254
+ s.phase = 4
255
+ return nil
256
+ end
257
+ if step == 4 then
258
+ s.joined = true
259
+ fx.join(s)
260
+ return nil
261
+ end
262
+ if step == 5 then
263
+ s.start = 1
264
+ local thread = coroutine.create(runStart)
265
+ s.startThread = thread
266
+ own(thread, s)
267
+ local ok, err = coroutine.resume(thread, s)
268
+ if not ok then
269
+ note(s, 3, err)
270
+ end
271
+ return nil
272
+ end
273
+ if step == 6 then
274
+ s.ticking = true
275
+ fx.tick(s)
276
+ return nil
277
+ end
278
+ if step == 7 then
279
+ if s.ticking then
280
+ s.ticking = false
281
+ fx.untick(s)
282
+ end
283
+ if s.joined then
284
+ s.joined = false
285
+ fx.leave(s)
286
+ end
287
+ return nil
288
+ end
289
+ if step == 8 then
290
+ s.phase = 4
291
+ run(s, 4)
292
+ return nil
293
+ end
294
+ if step == 9 then
295
+ local thread = s.startThread
296
+ s.startThread = nil
297
+ if s.start == 1 and thread ~= nil then
298
+ stopStart(s, thread)
299
+ end
300
+ run(s, 5)
301
+ finish(s)
302
+ return nil
303
+ end
304
+ if step == 10 then
305
+ finish(s)
306
+ return nil
307
+ end
308
+ until true
309
+ end
310
+ --* Ends a generation with no user hooks, after its own teardown failed again. Every effect is protected.
311
+ local function forceEnd(s)
312
+ local fx = effects()
313
+ local thread = s.startThread
314
+ s.startThread = nil
315
+ if s.start == 1 and thread ~= nil then
316
+ pcall(stopStart, s, thread)
317
+ end
318
+ if s.ticking then
319
+ s.ticking = false
320
+ pcall(fx.untick, s)
321
+ end
322
+ if s.joined then
323
+ s.joined = false
324
+ pcall(fx.leave, s)
325
+ end
326
+ s.phase = if s.fault == nil then 6 else 5
327
+ pcall(release, s)
328
+ pcall(fx.ended, s)
329
+ end
330
+ --* @internal Steps one causal chain may take on one object before it faults. Fault cleanup runs past it.
331
+ local MAX_TRANSITIONS = 64
332
+ --* @internal Creation that never settles is a failure after this many awake waves in one batch.
333
+ local MAX_AWAKE_WAVES = 1024
334
+ --* @internal Batches one drain may start. A chain of objects creating objects in `start` stops here.
335
+ local MAX_BATCHES = 1024
336
+ --[[
337
+ *
338
+ * @internal
339
+ * Requests that steps may make in one drain. C5 bounds an object that undoes itself, not objects that undo each
340
+ * other; this cap ends such a cycle. Like the batch cap it is a failure, and its value waits on a Studio measurement.
341
+
342
+ ]]
343
+ local MAX_CHAINED_REQUESTS = 65_536
344
+ --* @internal Reconcile rounds one drain may run. Binding that queues pairs on every round stops here.
345
+ local MAX_RECONCILE_ROUNDS = 1024
346
+ --* @internal Consecutive internal errors that fault or force-end nothing before the drain gives up on its work.
347
+ local MAX_RECOVERIES = 1
348
+ local function awaking(batch)
349
+ return batch.sweep == 0 or batch.sweep == 1
350
+ end
351
+ local function settle(reconciler)
352
+ reconciler:settle()
353
+ end
354
+ --[[
355
+ *
356
+ * The single reconciler. Every lifecycle and binding change happens in a drain, and one drain runs at a time.
357
+ * A drain starts at an outermost entry point: an engine signal, boot, a frame dispatch, or a public call made
358
+ * outside a drain. A request made inside a drain only enqueues, so no hook ever runs while another object's
359
+ * hook is on the stack. The stack holds objects to settle and batches to advance. What one step asks for
360
+ * collects in `fresh` and moves onto the stack when the step ends, first request on top. So requests run in
361
+ * the order they were made, after the hook that made them returns and before its object takes its next step.
362
+
363
+ ]]
364
+ local request, fault
365
+ local Reconciler
366
+ do
367
+ Reconciler = setmetatable({}, {
368
+ __tostring = function()
369
+ return "Reconciler"
370
+ end,
371
+ })
372
+ Reconciler.__index = Reconciler
373
+ function Reconciler.new(...)
374
+ local self = setmetatable({}, Reconciler)
375
+ return self:constructor(...) or self
376
+ end
377
+ function Reconciler:constructor()
378
+ self.stack = {}
379
+ self.fresh = {}
380
+ self.depth = 0
381
+ self.pairsQueued = false
382
+ self.rounds = 0
383
+ self.batches = 0
384
+ self.requests = 0
385
+ end
386
+ function Reconciler:isDraining()
387
+ return self.depth > 0
388
+ end
389
+ function Reconciler:enter(callback, a, b, owner)
390
+ self.depth += 1
391
+ local result, value = call(callback, a, b, owner)
392
+ self.depth -= 1
393
+ return result, value
394
+ end
395
+ function Reconciler:fail(message)
396
+ if self.failure == nil then
397
+ self.failure = message
398
+ end
399
+ end
400
+ function Reconciler:notePairs()
401
+ self.pairsQueued = true
402
+ end
403
+ function Reconciler:push(s)
404
+ if s.queued then
405
+ return nil
406
+ end
407
+ s.queued = true
408
+ local _fresh = self.fresh
409
+ local _s = s
410
+ table.insert(_fresh, _s)
411
+ end
412
+ function Reconciler:chain(s)
413
+ local stepping = self.stepping
414
+ if stepping ~= s then
415
+ s.transitions = 0
416
+ end
417
+ if stepping == nil then
418
+ return nil
419
+ end
420
+ self.requests += 1
421
+ if self.requests <= MAX_CHAINED_REQUESTS or stepping.fault ~= nil then
422
+ return nil
423
+ end
424
+ -- A cycle's members keep requesting while a passive target never does, so the requester is the one to end.
425
+ self.requests = 0
426
+ local message = `Crucible: objects kept changing each other through {MAX_CHAINED_REQUESTS} requests in one drain`
427
+ self:fail(message)
428
+ note(stepping, 9, message)
429
+ end
430
+ function Reconciler:raise(s, gate)
431
+ if s.gate < gate then
432
+ s.gate = gate
433
+ end
434
+ self:push(s)
435
+ end
436
+ function Reconciler:innermostBatch()
437
+ for i = #self.fresh - 1, 0, -1 do
438
+ local entry = self.fresh[i + 1]
439
+ if entry.work == 1 then
440
+ return entry
441
+ end
442
+ end
443
+ for i = #self.stack - 1, 0, -1 do
444
+ local entry = self.stack[i + 1]
445
+ if entry.work == 1 then
446
+ return entry
447
+ end
448
+ end
449
+ return nil
450
+ end
451
+ function Reconciler:admit(created)
452
+ if #created == 0 then
453
+ return nil
454
+ end
455
+ local open = self:innermostBatch()
456
+ if open ~= nil and awaking(open) then
457
+ for _, s in created do
458
+ local _exp = open.pending
459
+ table.insert(_exp, s)
460
+ end
461
+ return nil
462
+ end
463
+ if self.batches == MAX_BATCHES then
464
+ for _, s in created do
465
+ request(s, 2)
466
+ end
467
+ self:fail(`Crucible: objects kept creating objects for {MAX_BATCHES} batches in one drain; the rest were dropped`)
468
+ return nil
469
+ end
470
+ self.batches += 1
471
+ local _fresh = self.fresh
472
+ local _object = {
473
+ work = 1,
474
+ members = {},
475
+ wave = {},
476
+ }
477
+ local _left = "pending"
478
+ local _array = {}
479
+ local _length = #_array
480
+ table.move(created, 1, #created, _length + 1, _array)
481
+ _object[_left] = _array
482
+ _object.sweep = 1
483
+ _object.cursor = 0
484
+ _object.waves = 0
485
+ table.insert(_fresh, _object)
486
+ self:drain()
487
+ end
488
+ function Reconciler:abandon()
489
+ local _array = {}
490
+ local _length = #_array
491
+ local _array_1 = self.stack
492
+ table.move(_array_1, 1, #_array_1, _length + 1, _array)
493
+ local _array_2 = {}
494
+ local _length_1 = #_array_2
495
+ local _array_3 = self.fresh
496
+ table.move(_array_3, 1, #_array_3, _length_1 + 1, _array_2)
497
+ for _, list in { _array, _array_2 } do
498
+ for _1, entry in list do
499
+ if entry.work ~= 1 then
500
+ continue
501
+ end
502
+ for _2, s in entry.members do
503
+ request(s, 2)
504
+ end
505
+ for _2, s in entry.pending do
506
+ request(s, 2)
507
+ end
508
+ end
509
+ end
510
+ end
511
+ function Reconciler:advance(batch)
512
+ local _exp = batch.sweep
513
+ repeat
514
+ local _fallthrough = false
515
+ if _exp == 0 then
516
+ _fallthrough = true
517
+ end
518
+ if _fallthrough or _exp == 1 then
519
+ if batch.cursor < #batch.wave then
520
+ local s = batch.wave[batch.cursor + 1]
521
+ batch.cursor += 1
522
+ self:raise(s, if batch.sweep == 0 then 1 else 2)
523
+ return true
524
+ end
525
+ batch.cursor = 0
526
+ if batch.sweep == 0 then
527
+ batch.sweep = 1
528
+ return true
529
+ end
530
+ local wave = batch.pending
531
+ batch.pending = {}
532
+ if #wave == 0 then
533
+ batch.sweep = 2
534
+ elseif batch.waves == MAX_AWAKE_WAVES then
535
+ for _, s in wave do
536
+ request(s, 2)
537
+ end
538
+ self:fail(`Crucible: objects kept creating objects during awake for {MAX_AWAKE_WAVES} waves; the last wave was dropped`)
539
+ batch.sweep = 2
540
+ else
541
+ batch.waves += 1
542
+ batch.wave = wave
543
+ for _, s in wave do
544
+ local _exp_1 = batch.members
545
+ table.insert(_exp_1, s)
546
+ end
547
+ batch.sweep = 0
548
+ end
549
+ return true
550
+ end
551
+ if _exp == 2 then
552
+ _fallthrough = true
553
+ end
554
+ if _fallthrough or _exp == 3 then
555
+ if batch.cursor < #batch.members then
556
+ local s = batch.members[batch.cursor + 1]
557
+ batch.cursor += 1
558
+ self:raise(s, if batch.sweep == 2 then 3 else 4)
559
+ return true
560
+ end
561
+ if batch.sweep == 3 then
562
+ return false
563
+ end
564
+ batch.cursor = 0
565
+ batch.sweep = 3
566
+ return true
567
+ end
568
+ until true
569
+ return false
570
+ end
571
+ function Reconciler:settleStep(s)
572
+ local step = nextStep(s)
573
+ if step == nil then
574
+ return false
575
+ end
576
+ s.transitions += 1
577
+ if s.fault == nil then
578
+ if s.transitions > MAX_TRANSITIONS then
579
+ note(s, 9, `did not settle after {MAX_TRANSITIONS} transitions since a request from outside it`)
580
+ return true
581
+ end
582
+ end
583
+ self.stepping = s
584
+ perform(s, step)
585
+ self.stepping = nil
586
+ return true
587
+ end
588
+ function Reconciler:settle()
589
+ while true do
590
+ while #self.fresh > 0 do
591
+ local _stack = self.stack
592
+ local _exp = self.fresh
593
+ -- ▼ Array.pop ▼
594
+ local _length = #_exp
595
+ local _result = _exp[_length]
596
+ _exp[_length] = nil
597
+ -- ▲ Array.pop ▲
598
+ table.insert(_stack, _result)
599
+ end
600
+ local top = self.stack[#self.stack]
601
+ if top == nil or top.work == 1 then
602
+ if self.pairsQueued then
603
+ self.pairsQueued = false
604
+ if self.rounds == MAX_RECONCILE_ROUNDS then
605
+ self:fail(`Crucible: binding kept queueing pairs for {MAX_RECONCILE_ROUNDS} reconcile rounds in one drain; the rest wait for the next drain`)
606
+ else
607
+ self.rounds += 1
608
+ self:admit(effects().reconcile())
609
+ end
610
+ continue
611
+ end
612
+ if top == nil then
613
+ return nil
614
+ end
615
+ if not self:advance(top) then
616
+ local _exp = self.stack
617
+ _exp[#_exp] = nil
618
+ end
619
+ continue
620
+ end
621
+ if not self:settleStep(top) then
622
+ local _exp = self.stack
623
+ _exp[#_exp] = nil
624
+ top.queued = false
625
+ end
626
+ end
627
+ end
628
+ function Reconciler:unwind()
629
+ local stranded = {}
630
+ local rest = {}
631
+ local _array = {}
632
+ local _length = #_array
633
+ local _array_1 = self.fresh
634
+ table.move(_array_1, 1, #_array_1, _length + 1, _array)
635
+ local order = _array
636
+ for i = #self.stack - 1, 0, -1 do
637
+ local _arg0 = self.stack[i + 1]
638
+ table.insert(order, _arg0)
639
+ end
640
+ table.clear(self.fresh)
641
+ table.clear(self.stack)
642
+ for _, entry in order do
643
+ if entry.work == 0 then
644
+ entry.queued = false
645
+ table.insert(rest, entry)
646
+ continue
647
+ end
648
+ for _1, s in entry.members do
649
+ if s.gate < 4 then
650
+ table.insert(stranded, s)
651
+ end
652
+ end
653
+ for _1, s in entry.pending do
654
+ if s.gate < 4 then
655
+ table.insert(stranded, s)
656
+ end
657
+ end
658
+ end
659
+ return stranded, rest
660
+ end
661
+ function Reconciler:drain()
662
+ if self.depth > 0 or (#self.stack == 0 and #self.fresh == 0 and not self.pairsQueued) then
663
+ return nil
664
+ end
665
+ local errors = {}
666
+ local faulted = {}
667
+ local idle = 0
668
+ local result, err = self:enter(settle, self, nil)
669
+ while result ~= 0 do
670
+ local _arg0 = tostring(err)
671
+ table.insert(errors, _arg0)
672
+ local culprit = self.stepping
673
+ self.stepping = nil
674
+ local stranded, rest = self:unwind()
675
+ local progressed = false
676
+ if culprit ~= nil and not hasEnded(culprit) then
677
+ if faulted[culprit] ~= nil then
678
+ forceEnd(culprit)
679
+ else
680
+ faulted[culprit] = true
681
+ note(culprit, 11, err)
682
+ end
683
+ progressed = true
684
+ end
685
+ for _, s in stranded do
686
+ if hasEnded(s) or faulted[s] ~= nil then
687
+ continue
688
+ end
689
+ faulted[s] = true
690
+ note(s, 11, err)
691
+ progressed = true
692
+ end
693
+ idle = if progressed then 0 else idle + 1
694
+ local _array = {}
695
+ local _length = #_array
696
+ local _strandedLength = #stranded
697
+ table.move(stranded, 1, _strandedLength, _length + 1, _array)
698
+ _length += _strandedLength
699
+ table.move(rest, 1, #rest, _length + 1, _array)
700
+ -- ▼ ReadonlyArray.filter ▼
701
+ local _newValue = {}
702
+ local _callback = function(s)
703
+ return not hasEnded(s)
704
+ end
705
+ local _length_1 = 0
706
+ for _k, _v in _array do
707
+ if _callback(_v, _k - 1, _array) == true then
708
+ _length_1 += 1
709
+ _newValue[_length_1] = _v
710
+ end
711
+ end
712
+ -- ▲ ReadonlyArray.filter ▲
713
+ local left = _newValue
714
+ if idle > MAX_RECOVERIES then
715
+ self.pairsQueued = false
716
+ for _, s in left do
717
+ if hasEnded(s) then
718
+ continue
719
+ end
720
+ if not (faulted[s] ~= nil) then
721
+ note(s, 11, err)
722
+ end
723
+ forceEnd(s)
724
+ end
725
+ break
726
+ end
727
+ for _, s in left do
728
+ self:push(s)
729
+ end
730
+ result, err = self:enter(settle, self, nil)
731
+ end
732
+ self.rounds = 0
733
+ self.batches = 0
734
+ self.requests = 0
735
+ self.stepping = nil
736
+ local failure = self.failure
737
+ self.failure = nil
738
+ local fx = effects()
739
+ for _, e in errors do
740
+ fx.failed(`Crucible: internal error; the reconciler recovered: {e}`)
741
+ end
742
+ if failure ~= nil then
743
+ fx.failed(failure)
744
+ end
745
+ end
746
+ function Reconciler:within(owner, blame, hook, callback, a, b)
747
+ local outer = self.depth > 0
748
+ local result, value = self:enter(callback, a, b, owner)
749
+ if result ~= 0 and blame ~= nil then
750
+ fault(blame, hook, value)
751
+ end
752
+ if not outer then
753
+ self:drain()
754
+ end
755
+ return result, value
756
+ end
757
+ end
758
+ local reconciler = Reconciler.new()
759
+ --* @internal True while a drain runs, including inside every hook and callback it calls.
760
+ local function isDraining()
761
+ return reconciler:isDraining()
762
+ end
763
+ --[[
764
+ *
765
+ * @internal
766
+ * Runs every queued change to rest. Returns at once when a drain is already running: that drain processes the
767
+ * queue before it returns. Never throws. A cap or an internal error goes to `effects.failed` after recovery.
768
+
769
+ ]]
770
+ local function drain()
771
+ reconciler:drain()
772
+ end
773
+ --[[
774
+ *
775
+ * @internal
776
+ * Binding has queued pairs. The drain reconciles them when its top is a batch or it is empty. Past the round
777
+ * cap the drain drops the note, so binding calls this again whenever it queues.
778
+
779
+ ]]
780
+ local function notePairs()
781
+ reconciler:notePairs()
782
+ end
783
+ --[[
784
+ *
785
+ * @internal
786
+ * Runs one callback as its own scope: a frame hook, an `All<T>` visit, or an observer callback. Requests it
787
+ * makes apply after it returns. A failure faults `blame`; with no blame the caller logs the returned failure.
788
+ * Outside a drain, the scope drains when it ends, so the next frame hook in a lane sees the effects of the one
789
+ * before.
790
+
791
+ ]]
792
+ local function within(owner, blame, hook, callback, a, b)
793
+ return reconciler:within(owner, blame, hook, callback, a, b)
794
+ end
795
+ --* @internal Updates `want` and queues the object. Called by `setEnabled`, `remove()` and binding. Ignored once Destroyed is wanted.
796
+ function request(s, want)
797
+ if s.want == 2 then
798
+ return nil
799
+ end
800
+ s.want = want
801
+ reconciler:chain(s)
802
+ reconciler:push(s)
803
+ reconciler:drain()
804
+ end
805
+ --* @internal Faults the object from outside a lifecycle step: a frame hook, `accepts`, or an `All<T>` callback.
806
+ function fault(s, hook, err)
807
+ if hasEnded(s) then
808
+ return nil
809
+ end
810
+ note(s, hook, err)
811
+ reconciler:chain(s)
812
+ reconciler:push(s)
813
+ reconciler:drain()
814
+ end
815
+ --* @internal Called by the start thread when `start` returns or throws. A start that outlived its generation is ignored.
816
+ function finishStart(s, ok, err)
817
+ if hasEnded(s) then
818
+ return nil
819
+ end
820
+ s.startThread = nil
821
+ s.start = 2
822
+ if not ok then
823
+ note(s, 3, err)
824
+ end
825
+ reconciler:chain(s)
826
+ reconciler:push(s)
827
+ reconciler:drain()
828
+ end
829
+ --[[
830
+ *
831
+ * @internal
832
+ * Gives new generations a batch. Inside the awake closure of the innermost batch they join its next wave, so
833
+ * every object a batch creates during awake is awake before any of the batch is enabled. Otherwise they form
834
+ * a new batch, which runs to rest before the code that created them continues.
835
+
836
+ ]]
837
+ local function admit(created)
838
+ reconciler:admit(created)
839
+ end
840
+ --* @internal Asks every generation in an unfinished batch to end. Boot calls it when a Subsystem faults.
841
+ local function abandon()
842
+ reconciler:abandon()
843
+ end
844
+ return {
845
+ nextStep = nextStep,
846
+ useEffects = useEffects,
847
+ stateOf = stateOf,
848
+ isLive = isLive,
849
+ hasEnded = hasEnded,
850
+ track = track,
851
+ current = current,
852
+ isDraining = isDraining,
853
+ drain = drain,
854
+ notePairs = notePairs,
855
+ within = within,
856
+ request = request,
857
+ fault = fault,
858
+ finishStart = finishStart,
859
+ admit = admit,
860
+ abandon = abandon,
861
+ MAX_TRANSITIONS = MAX_TRANSITIONS,
862
+ MAX_AWAKE_WAVES = MAX_AWAKE_WAVES,
863
+ MAX_BATCHES = MAX_BATCHES,
864
+ MAX_CHAINED_REQUESTS = MAX_CHAINED_REQUESTS,
865
+ MAX_RECONCILE_ROUNDS = MAX_RECONCILE_ROUNDS,
866
+ MAX_RECOVERIES = MAX_RECOVERIES,
867
+ }