@flamework-experimental/core 2.0.0-alpha.1 → 2.0.0-alpha.3

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 (38) hide show
  1. package/README.md +35 -27
  2. package/flamework.build +3 -3
  3. package/out/dependency.d.ts +4 -0
  4. package/out/dependency.luau +4 -0
  5. package/out/flamework.luau +8 -0
  6. package/out/index.d.ts +4 -2
  7. package/out/init.luau +10 -2
  8. package/out/lifecycle/lifecyclePlugin.d.ts +175 -2
  9. package/out/lifecycle/lifecyclePlugin.luau +636 -102
  10. package/out/module/module.luau +337 -41
  11. package/out/module/moduleBuilder.d.ts +12 -3
  12. package/out/module/moduleBuilder.luau +22 -0
  13. package/out/module/moduleDefinition.d.ts +6 -0
  14. package/out/module/providerRegistration.d.ts +8 -0
  15. package/out/module/providerRegistration.luau +29 -0
  16. package/out/plugin/pluginDefinition.d.ts +14 -4
  17. package/out/provider.d.ts +19 -0
  18. package/out/provider.luau +8 -0
  19. package/out/reflect.luau +17 -0
  20. package/out/utility/explainUnresolved.d.ts +9 -0
  21. package/out/utility/explainUnresolved.luau +43 -0
  22. package/out/utility/getClassImplements.d.ts +9 -2
  23. package/out/utility/getClassImplements.luau +89 -6
  24. package/out/utility/getClassesInPath.d.ts +10 -2
  25. package/out/utility/getClassesInPath.luau +70 -31
  26. package/out/utility/globs.d.ts +2 -2
  27. package/out/utility/globs.luau +3 -3
  28. package/out/utility/implementsCache.d.ts +16 -0
  29. package/out/utility/implementsCache.luau +35 -0
  30. package/out/utility/leftOut.d.ts +35 -0
  31. package/out/utility/leftOut.luau +171 -0
  32. package/out/utility/moduleClasses.d.ts +9 -0
  33. package/out/utility/moduleClasses.luau +64 -0
  34. package/out/utility/recycleThread.d.ts +12 -0
  35. package/out/utility/recycleThread.luau +29 -10
  36. package/out/utility/threadWaits.d.ts +36 -0
  37. package/out/utility/threadWaits.luau +81 -0
  38. package/package.json +1 -1
@@ -5,11 +5,20 @@ local convertConciseDependencyInfo = TS.import(script, script.Parent.Parent, "ut
5
5
  local getClassImplements = TS.import(script, script.Parent.Parent, "utility", "getClassImplements").getClassImplements
6
6
  local getClassesInPath = TS.import(script, script.Parent.Parent, "utility", "getClassesInPath").getClassesInPath
7
7
  local getClassesInGlob = TS.import(script, script.Parent.Parent, "utility", "globs").getClassesInGlob
8
+ local explainUnresolvedClass = TS.import(script, script.Parent.Parent, "utility", "explainUnresolved").explainUnresolvedClass
9
+ local _leftOut = TS.import(script, script.Parent.Parent, "utility", "leftOut")
10
+ local explainLeftOut = _leftOut.explainLeftOut
11
+ local leftOutRegistration = _leftOut.leftOutRegistration
12
+ local _threadWaits = TS.import(script, script.Parent.Parent, "utility", "threadWaits")
13
+ local extinguishesBegun = _threadWaits.extinguishesBegun
14
+ local threadWaits = _threadWaits.threadWaits
8
15
  local clearDefaultModule = TS.import(script, script.Parent, "defaultModule").clearDefaultModule
9
16
  local HookPriority = TS.import(script, script.Parent, "moduleHooks").HookPriority
10
17
  local _providerRegistration = TS.import(script, script.Parent, "providerRegistration")
18
+ local DEFAULT_LOAD_ORDER = _providerRegistration.DEFAULT_LOAD_ORDER
11
19
  local getProviderClassId = _providerRegistration.getProviderClassId
12
20
  local getProviderClassScope = _providerRegistration.getProviderClassScope
21
+ local getProviderLoadOrder = _providerRegistration.getProviderLoadOrder
13
22
  local normalizeProviderConfig = _providerRegistration.normalizeProviderConfig
14
23
  local _scopes = TS.import(script, script.Parent, "scopes")
15
24
  local NO_CONDITION = _scopes.NO_CONDITION
@@ -73,6 +82,12 @@ local function createModuleInstantiation(state, options)
73
82
  local providers = {}
74
83
  --* The registrations left out, by id, with the conditions that were judged: for the error a miss gets.
75
84
  local skipped = {}
85
+ --* The path and glob registrations left out by their own condition, the builder's and the plugins'.
86
+ local _array_2 = {}
87
+ local _length_1 = #_array_2
88
+ local _array_3 = (state.leftOut or {})
89
+ table.move(_array_3, 1, #_array_3, _length_1 + 1, _array_2)
90
+ local leftOut = _array_2
76
91
  --* Modules searched after this one's own providers, in order. Ignited before this one, and extinguished after.
77
92
  local _result = options
78
93
  if _result ~= nil then
@@ -88,12 +103,24 @@ local function createModuleInstantiation(state, options)
88
103
  local instantiatedProviders = {}
89
104
  --* Objects handed over by `provideInstance`, attached to their interfaces once every plugin is set up.
90
105
  local providedInstances = {}
106
+ --[[
107
+ *
108
+ * Provided objects that have not joined their interfaces: until the ignition reaches them, and
109
+ * for good when an observer refuses one. `release` has nothing to take them out of, and a failed
110
+ * ignition told every observer `onRemoved` for objects it had never been told of, and told the
111
+ * observers that had just undone a refused one of it again.
112
+
113
+ ]]
114
+ local unjoinedInstances = {}
91
115
  local observers = {}
92
116
  local hooks = {}
93
117
  local includedPlugins = {}
94
118
  local filledSlots = {}
95
119
  local temporaryInstances = {}
96
120
  local moduleInitState = ModuleInitState.Created
121
+ --* The thread `extinguish` runs on, and what wakes each thread waiting in `awaitExtinguished` for it to finish.
122
+ local extinguishingThread
123
+ local extinguishWaiters = {}
97
124
  local switchInitState = function(from, to)
98
125
  if moduleInitState ~= from then
99
126
  error(`module '{state.debugName}' is in invalid state when transitioning to '{ModuleInitState[to]}', got '{ModuleInitState[moduleInitState]}' when '{ModuleInitState[from]}' was expected.`)
@@ -214,6 +241,67 @@ local function createModuleInstantiation(state, options)
214
241
  end
215
242
  providers = active
216
243
  end
244
+ --[[
245
+ *
246
+ * The registrations in ascending `loadOrder`, registration order among equals. The same list
247
+ * when none sets one, which is every module that does not use it.
248
+
249
+ ]]
250
+ local inLoadOrder = function(entries)
251
+ -- ▼ ReadonlyArray.map ▼
252
+ local _newValue = table.create(#entries)
253
+ local _callback = function(entry)
254
+ local _condition_1 = getProviderLoadOrder(entry.config)
255
+ if _condition_1 == nil then
256
+ _condition_1 = DEFAULT_LOAD_ORDER
257
+ end
258
+ return _condition_1
259
+ end
260
+ for _k, _v in entries do
261
+ _newValue[_k] = _callback(_v, _k - 1, entries)
262
+ end
263
+ -- ▲ ReadonlyArray.map ▲
264
+ local orders = _newValue
265
+ -- ▼ ReadonlyArray.every ▼
266
+ local _result_1 = true
267
+ local _callback_1 = function(order)
268
+ return order == DEFAULT_LOAD_ORDER
269
+ end
270
+ for _k, _v in orders do
271
+ if not _callback_1(_v, _k - 1, orders) then
272
+ _result_1 = false
273
+ break
274
+ end
275
+ end
276
+ -- ▲ ReadonlyArray.every ▲
277
+ if _result_1 then
278
+ return entries
279
+ end
280
+ -- `table.sort` is not stable, so equals are ordered by their position.
281
+ -- ▼ ReadonlyArray.map ▼
282
+ local _newValue_1 = table.create(#entries)
283
+ local _callback_2 = function(_, index)
284
+ return index
285
+ end
286
+ for _k, _v in entries do
287
+ _newValue_1[_k] = _callback_2(_v, _k - 1, entries)
288
+ end
289
+ -- ▲ ReadonlyArray.map ▲
290
+ local indices = _newValue_1
291
+ table.sort(indices, function(a, b)
292
+ return if orders[a + 1] ~= orders[b + 1] then orders[a + 1] < orders[b + 1] else a < b
293
+ end)
294
+ -- ▼ ReadonlyArray.map ▼
295
+ local _newValue_2 = table.create(#indices)
296
+ local _callback_3 = function(index)
297
+ return entries[index + 1]
298
+ end
299
+ for _k, _v in indices do
300
+ _newValue_2[_k] = _callback_3(_v, _k - 1, indices)
301
+ end
302
+ -- ▲ ReadonlyArray.map ▲
303
+ return _newValue_2
304
+ end
217
305
  local registerHook = function(phase, callback, priority)
218
306
  local _object = {
219
307
  phase = phase,
@@ -233,11 +321,11 @@ local function createModuleInstantiation(state, options)
233
321
  local _callback = function(hook)
234
322
  return hook.phase == phase
235
323
  end
236
- local _length_1 = 0
324
+ local _length_2 = 0
237
325
  for _k, _v in hooks do
238
326
  if _callback(_v, _k - 1, hooks) == true then
239
- _length_1 += 1
240
- _newValue[_length_1] = _v
327
+ _length_2 += 1
328
+ _newValue[_length_2] = _v
241
329
  end
242
330
  end
243
331
  -- ▲ ReadonlyArray.filter ▲
@@ -292,12 +380,18 @@ local function createModuleInstantiation(state, options)
292
380
  * observer that raises from its `onAdded` has the ones before it told `onRemoved`, and the
293
381
  * error comes out, so a refused object is attached nowhere -- rather than left ticking in the
294
382
  * lifecycle's sets, with no handle to remove it by.
383
+ *
384
+ * `dependencies`, what a provider's constructor was given, is handed to the observers with it.
295
385
 
296
386
  ]]
297
- local registerClassInterfaces = function(instance, kind)
298
- local added = {}
387
+ local registerClassInterfaces = function(instance, kind, dependencies, loadOrder)
388
+ local interfaces = getClassImplements(instance)
389
+ -- How many observers have been told `onAdded`, in the order they were told: what a refusal
390
+ -- undoes. Counted rather than recorded, since an attachment that goes through -- every one,
391
+ -- in a game -- then builds nothing to throw away.
392
+ local attached = 0
299
393
  local success, err = pcall(function()
300
- for _, interfaceId in getClassImplements(instance) do
394
+ for _, interfaceId in interfaces do
301
395
  local interested = observers[interfaceId]
302
396
  if not interested then
303
397
  continue
@@ -308,18 +402,36 @@ local function createModuleInstantiation(state, options)
308
402
  _result_1(instance, {
309
403
  interfaceId = interfaceId,
310
404
  kind = kind,
405
+ dependencies = dependencies,
406
+ loadOrder = loadOrder,
311
407
  })
312
408
  end
313
- local _arg0 = { interfaceId, observer }
314
- table.insert(added, _arg0)
409
+ attached += 1
315
410
  end
316
411
  end
317
412
  end)
318
413
  if not success then
414
+ -- The observers told, found again by walking the same way as far as the count goes.
415
+ local told = {}
416
+ for _, interfaceId in interfaces do
417
+ local interested = observers[interfaceId]
418
+ if not interested then
419
+ continue
420
+ end
421
+ for _1, observer in interested do
422
+ if #told == attached then
423
+ break
424
+ end
425
+ local _arg0 = { interfaceId, observer }
426
+ table.insert(told, _arg0)
427
+ end
428
+ end
319
429
  -- Undone in reverse, each step guarded, so that one `onRemoved` raising does not leave
320
- -- the rest attached; the observer's error is what comes out.
321
- for i = #added - 1, 0, -1 do
322
- local _binding = added[i + 1]
430
+ -- the rest attached; the observer's error is what comes out. Marked as refused, so that
431
+ -- an observer owing a departing object a last event -- the lifecycle plugin's
432
+ -- `onExtinguished` while the module extinguishes -- does not deliver it.
433
+ for i = #told - 1, 0, -1 do
434
+ local _binding = told[i + 1]
323
435
  local interfaceId = _binding[1]
324
436
  local observer = _binding[2]
325
437
  guarded("undoing an attachment", function()
@@ -328,6 +440,7 @@ local function createModuleInstantiation(state, options)
328
440
  _result_1 = _result_1(instance, {
329
441
  interfaceId = interfaceId,
330
442
  kind = kind,
443
+ refused = true,
331
444
  })
332
445
  end
333
446
  return _result_1
@@ -353,11 +466,15 @@ local function createModuleInstantiation(state, options)
353
466
  end
354
467
  end
355
468
  end
469
+ --* Constructs a class, resolving what its constructor takes into `resolvedParameters`.
356
470
  local resolveDependencyWithOrigin
357
- local instantiateClassWithDependencies = function(constructor, resolve)
471
+ local instantiateClassWithDependencies = function(constructor, resolve, resolvedParameters)
472
+ if resolvedParameters == nil then
473
+ resolvedParameters = {}
474
+ end
358
475
  local dependencies = Reflect.getMetadata(constructor, "flamework:dependencies") or {}
359
- local resolvedParameters = {}
360
476
  for _, dependency in dependencies do
477
+ local _resolvedParameters = resolvedParameters
361
478
  local _result_1 = resolve
362
479
  if _result_1 ~= nil then
363
480
  _result_1 = _result_1(dependency)
@@ -366,7 +483,7 @@ local function createModuleInstantiation(state, options)
366
483
  if _condition_1 == nil then
367
484
  _condition_1 = resolveDependencyWithOrigin(dependency, constructor)
368
485
  end
369
- table.insert(resolvedParameters, _condition_1)
486
+ table.insert(_resolvedParameters, _condition_1)
370
487
  end
371
488
  return constructor.new(unpack(resolvedParameters))
372
489
  end
@@ -405,10 +522,21 @@ local function createModuleInstantiation(state, options)
405
522
  if moduleProvider then
406
523
  local config = moduleProvider.config
407
524
  if config.type == "class" then
408
- local instantiatedProvider = instantiateClassWithDependencies(config.value)
525
+ local dependencies = {}
526
+ local instantiatedProvider = instantiateClassWithDependencies(config.value, nil, dependencies)
409
527
  local _id_1 = info.id
410
528
  instantiatedProviders[_id_1] = instantiatedProvider
411
- registerClassInterfaces(instantiatedProvider, "provider")
529
+ -- Held only if it is attached: one an observer refuses is attached nowhere, so it must
530
+ -- not be cached either -- the next resolve handed it out with no lifecycle at all, and
531
+ -- `release` told every observer, the refusing one included, it was removed again.
532
+ local attached, err = pcall(function()
533
+ return registerClassInterfaces(instantiatedProvider, "provider", dependencies, getProviderLoadOrder(config))
534
+ end)
535
+ if not attached then
536
+ local _id_2 = info.id
537
+ instantiatedProviders[_id_2] = nil
538
+ error(err, 0)
539
+ end
412
540
  return instantiatedProvider
413
541
  elseif config.type == "function" then
414
542
  -- Function providers are not cached.
@@ -456,6 +584,17 @@ local function createModuleInstantiation(state, options)
456
584
  if inactive ~= nil then
457
585
  error(`module '{state.debugName}' could not resolve dependency '{info.id}': it is registered but inactive ({describeConditions(inactive)}){searched}`)
458
586
  end
587
+ -- A folder registration left out by its own condition registered nothing to be inactive.
588
+ local leftOutReason = explainLeftOut(info.id, leftOut)
589
+ if leftOutReason ~= nil then
590
+ error(`module '{state.debugName}' could not resolve dependency '{info.id}': {leftOutReason}{searched}`)
591
+ end
592
+ -- A class that has been loaded says why it is not here: a component, a provider nothing
593
+ -- registered, a class that is not a provider at all.
594
+ local explanation = explainUnresolvedClass(info.id, requestingOrigin)
595
+ if explanation ~= nil then
596
+ error(`module '{state.debugName}' could not resolve dependency '{info.id}': {explanation}{searched}`)
597
+ end
459
598
  error(`module '{state.debugName}' could not resolve dependency '{info.id}'{searched}`)
460
599
  end
461
600
  return dependency
@@ -602,14 +741,27 @@ local function createModuleInstantiation(state, options)
602
741
  -- every plugin's observers are in place.
603
742
  for _, instance in providedInstances do
604
743
  registerClassInterfaces(instance, "provider")
744
+ unjoinedInstances[instance] = nil
605
745
  end
606
- for _, provider in providers do
746
+ -- In ascending `loadOrder`, each after what its constructor takes, which is the order the
747
+ -- lifecycle plugin runs `onInit` in: a low `loadOrder` goes first and pulls its
748
+ -- dependencies forward with it, and dependency order still wins over `loadOrder`.
749
+ for _, provider in inLoadOrder(providers) do
607
750
  -- Lazy providers are constructed the first time they are resolved instead.
608
751
  if provider.config.type == "class" and provider.config.lazy ~= true then
609
752
  resolveDependency(provider.injectionId)
610
753
  end
611
754
  end
612
755
  runHooks("postIgnite")
756
+ -- Checked again once everything has run: an `onInit` that yields lets other threads run in
757
+ -- the middle of ignition, and one that extinguished an import then did not see this module
758
+ -- among its importers, since it joins them only below. Carrying on left it ignited onto a
759
+ -- released import that would never take it down.
760
+ for _, imported in imports do
761
+ if not imported.isIgnited() then
762
+ error(`module '{state.debugName}': imported module '{imported.debugName}' was extinguished while this module was igniting`)
763
+ end
764
+ end
613
765
  end)
614
766
  if not success then
615
767
  moduleInitState = ModuleInitState.Extinguishing
@@ -621,21 +773,40 @@ local function createModuleInstantiation(state, options)
621
773
  for _, imported in imports do
622
774
  imported.addImporter(module)
623
775
  end
776
+ -- Ignition has completed, so this is where the lifecycle plugin starts the providers: an
777
+ -- `onStart` sees the module ignited, may extinguish it, and may ignite one that imports it.
778
+ -- Nothing here can fail the ignition, so a hook that raises is warned about. One that
779
+ -- extinguishes the module ends the phase: the hooks after it would set up a module gone.
780
+ for _, hook in sortedHooks("ignited") do
781
+ if moduleInitState ~= ModuleInitState.Ignited then
782
+ break
783
+ end
784
+ guarded("an onIgnited hook", function()
785
+ return hook.callback(module)
786
+ end)
787
+ end
624
788
  return module
625
789
  end
626
790
  local extinguish = function()
627
791
  switchInitState(ModuleInitState.Ignited, ModuleInitState.Extinguishing)
792
+ extinguishingThread = coroutine.running()
793
+ extinguishesBegun.count += 1
628
794
  -- Importers hold this module's instances, so they go first, and each takes its own importers
629
- -- down before it returns: the deepest goes first. Copied, since each removes itself.
630
- local _array_2 = {}
631
- local _length_1 = #_array_2
795
+ -- down before it returns: the deepest goes first. Copied, since each removes itself. One
796
+ -- extinguishing already, on another thread suspended in a handler that yields, is still
797
+ -- using this module's providers, so it is waited for: skipped, it had this module released
798
+ -- under it.
799
+ local _array_4 = {}
800
+ local _length_2 = #_array_4
632
801
  for _v in importers do
633
- _length_1 += 1
634
- _array_2[_length_1] = _v
802
+ _length_2 += 1
803
+ _array_4[_length_2] = _v
635
804
  end
636
- for _, importer in _array_2 do
805
+ for _, importer in _array_4 do
637
806
  if not importer.isExtinguished() then
638
807
  importer.extinguish()
808
+ else
809
+ importer.awaitExtinguished()
639
810
  end
640
811
  end
641
812
  table.clear(importers)
@@ -660,13 +831,13 @@ local function createModuleInstantiation(state, options)
660
831
  end)
661
832
  end
662
833
  -- Copied first: removal callbacks may themselves remove instances.
663
- local _array_2 = {}
664
- local _length_1 = #_array_2
834
+ local _array_4 = {}
835
+ local _length_2 = #_array_4
665
836
  for _v in temporaryInstances do
666
- _length_1 += 1
667
- _array_2[_length_1] = _v
837
+ _length_2 += 1
838
+ _array_4[_length_2] = _v
668
839
  end
669
- for _, temporaryInstance in _array_2 do
840
+ for _, temporaryInstance in _array_4 do
670
841
  guarded("removing an instance", function()
671
842
  return removeClassInstance(temporaryInstance)
672
843
  end)
@@ -674,10 +845,31 @@ local function createModuleInstantiation(state, options)
674
845
  -- Providers join their interfaces when they are instantiated, so they have to leave them
675
846
  -- too. Without this, a plugin such as the lifecycle plugin keeps holding (and ticking)
676
847
  -- providers that belong to an extinguished module.
677
- for _, provider in instantiatedProviders do
678
- guarded("removing a provider", function()
679
- return unregisterClassInterfaces(provider, "provider")
680
- end)
848
+ --
849
+ -- Over a copy, taken again until nothing new turns up: a removal runs user code -- the
850
+ -- lifecycle plugin tells a provider resolved since its walk `onExtinguished` as it leaves --
851
+ -- which may resolve a lazy provider for the first time, and a table gaining keys while it
852
+ -- is walked skipped providers, or had others unregistered twice. By object, not by id: an
853
+ -- object provided under several ids joined its interfaces once, so it leaves them once.
854
+ local released = {}
855
+ while true do
856
+ local remaining = {}
857
+ for _, provider in instantiatedProviders do
858
+ if not (released[provider] ~= nil) then
859
+ released[provider] = true
860
+ if not (unjoinedInstances[provider] ~= nil) then
861
+ table.insert(remaining, provider)
862
+ end
863
+ end
864
+ end
865
+ if #remaining == 0 then
866
+ break
867
+ end
868
+ for _, provider in remaining do
869
+ guarded("removing a provider", function()
870
+ return unregisterClassInterfaces(provider, "provider")
871
+ end)
872
+ end
681
873
  end
682
874
  -- A removal that raised leaves its instance behind, and nothing can add one any more.
683
875
  table.clear(temporaryInstances)
@@ -689,6 +881,80 @@ local function createModuleInstantiation(state, options)
689
881
  -- Released last, once nothing in here can resolve any more, so that the next root ignited
690
882
  -- becomes the default rather than `Dependency<T>()` answering from a dead module.
691
883
  clearDefaultModule(module)
884
+ for _, wake in extinguishWaiters do
885
+ wake()
886
+ end
887
+ table.clear(extinguishWaiters)
888
+ end
889
+ local awaitExtinguished = function()
890
+ if moduleInitState ~= ModuleInitState.Extinguishing then
891
+ return nil
892
+ end
893
+ -- On the extinguish's own thread -- one of its handlers extinguishing an import -- it cannot
894
+ -- finish until this returns, so waiting would never end. Nor on a thread the extinguish's
895
+ -- thread is itself waiting on here, however many waits away: two extinguishes, each waiting
896
+ -- for a module the other is taking down, held each other -- and every module between them,
897
+ -- still ticking -- for good. The wait that would close the circle is skipped, as the wait on
898
+ -- the extinguish's own thread is. The chain runs through an ignition waiting for an `onInit`
899
+ -- on its own thread too, which the lifecycle plugin records -- and through one waiting for the
900
+ -- Promise an `onInit` returned, whose work runs on a thread nothing can name: an `async`
901
+ -- method's body runs on one of its own. A chain that ends at one is taken to close the circle,
902
+ -- since the thread that settles it may be this one.
903
+ local running = coroutine.running()
904
+ local thread = extinguishingThread
905
+ local closesCircle = function()
906
+ local waitedOn = thread
907
+ while waitedOn ~= nil do
908
+ local _condition_1 = waitedOn == running
909
+ if not _condition_1 then
910
+ local _waitedOn = waitedOn
911
+ _condition_1 = not (type(_waitedOn) == "thread")
912
+ end
913
+ if _condition_1 then
914
+ return true
915
+ end
916
+ local _waitedOn = waitedOn
917
+ waitedOn = threadWaits[_waitedOn]
918
+ end
919
+ return false
920
+ end
921
+ if closesCircle() then
922
+ return nil
923
+ end
924
+ -- Nor on a thread that has died -- cancelled, as the testing runner cancels a body that
925
+ -- overran, or closed: that extinguish never finishes, and waiting for it held this module,
926
+ -- and everything in it, for good. One that dies while this waits is noticed within a frame.
927
+ if coroutine.status(thread) == "dead" then
928
+ return nil
929
+ end
930
+ local woken = false
931
+ local wake = function()
932
+ if woken then
933
+ return nil
934
+ end
935
+ woken = true
936
+ -- Removed here rather than by this thread once it resumes: one cancelled while it waits
937
+ -- never does, and left its entry behind for good.
938
+ threadWaits[running] = nil
939
+ if coroutine.status(running) ~= "dead" then
940
+ task.spawn(running)
941
+ end
942
+ end
943
+ threadWaits[running] = thread
944
+ table.insert(extinguishWaiters, wake)
945
+ -- Checked again every frame, as well as for a dead extinguish: this thread may be cancelled
946
+ -- while it waits, and the chain may come to close the circle after the wait began -- an
947
+ -- `async` `onInit`'s body waits here before the ignition that called it takes the Promise it
948
+ -- returned and waits on that.
949
+ task.spawn(function()
950
+ while not woken do
951
+ task.wait()
952
+ if coroutine.status(thread) == "dead" or coroutine.status(running) == "dead" or closesCircle() then
953
+ wake()
954
+ end
955
+ end
956
+ end)
957
+ coroutine.yield()
692
958
  end
693
959
  local isExtinguished = function()
694
960
  return moduleInitState >= ModuleInitState.Extinguishing
@@ -721,6 +987,7 @@ local function createModuleInstantiation(state, options)
721
987
  -- ▲ Set.delete ▲
722
988
  return _valueExisted
723
989
  end,
990
+ awaitExtinguished = awaitExtinguished,
724
991
  }
725
992
  --* What a plugin's setup is handed. Everything registers into this instantiation.
726
993
  pluginTarget = {
@@ -728,21 +995,35 @@ local function createModuleInstantiation(state, options)
728
995
  scope = if hasCondition(moduleScope) then moduleScope else nil,
729
996
  isActive = function(...)
730
997
  local conditions = { ... }
731
- local _array_2 = { moduleScope }
732
- local _length_1 = #_array_2
733
- table.move(conditions, 1, #conditions, _length_1 + 1, _array_2)
734
- return holdsEveryCondition(_array_2)
998
+ local _array_4 = { moduleScope }
999
+ local _length_2 = #_array_4
1000
+ table.move(conditions, 1, #conditions, _length_2 + 1, _array_4)
1001
+ return holdsEveryCondition(_array_4)
735
1002
  end,
736
1003
  registerClassProvider = registerClassProvider,
737
- registerProviders = function(_path, registrationOptions, resolved)
1004
+ registerProviders = function(path, registrationOptions, resolved)
738
1005
  local _arg0 = resolved ~= nil
739
1006
  assert(_arg0)
740
- registerProviderClasses(getClassesInPath(resolved), registrationOptions)
1007
+ if holdsCondition(registrationOptions) then
1008
+ registerProviderClasses(getClassesInPath(resolved), registrationOptions)
1009
+ else
1010
+ local _arg0_1 = leftOutRegistration(`registerProviders("{path}")`, registrationOptions, {
1011
+ path = resolved,
1012
+ })
1013
+ table.insert(leftOut, _arg0_1)
1014
+ end
741
1015
  end,
742
- registerProvidersGlob = function(_glob, registrationOptions, resolved)
1016
+ registerProvidersGlob = function(glob, registrationOptions, resolved)
743
1017
  local _arg0 = resolved ~= nil
744
1018
  assert(_arg0)
745
- registerProviderClasses(getClassesInGlob(resolved), registrationOptions)
1019
+ if holdsCondition(registrationOptions) then
1020
+ registerProviderClasses(getClassesInGlob(resolved), registrationOptions)
1021
+ else
1022
+ local _arg0_1 = leftOutRegistration(`registerProvidersGlob("{glob}")`, registrationOptions, {
1023
+ glob = resolved,
1024
+ })
1025
+ table.insert(leftOut, _arg0_1)
1026
+ end
746
1027
  end,
747
1028
  registerProvider = function(config, injectionId)
748
1029
  local _arg0 = injectionId ~= nil
@@ -760,8 +1041,15 @@ local function createModuleInstantiation(state, options)
760
1041
  local _injectionId = injectionId
761
1042
  local _instance = instance
762
1043
  instantiatedProviders[_injectionId] = _instance
1044
+ -- Once, however many ids it is provided under: it joins its interfaces once per entry,
1045
+ -- and the same object twice was initialised, started and told `onAdded` twice.
763
1046
  local _instance_1 = instance
764
- table.insert(providedInstances, _instance_1)
1047
+ if not (table.find(providedInstances, _instance_1) ~= nil) then
1048
+ local _instance_2 = instance
1049
+ table.insert(providedInstances, _instance_2)
1050
+ local _instance_3 = instance
1051
+ unjoinedInstances[_instance_3] = true
1052
+ end
765
1053
  end,
766
1054
  includePlugin = includePlugin,
767
1055
  onPreIgnite = function(callback, hookOptions)
@@ -780,6 +1068,14 @@ local function createModuleInstantiation(state, options)
780
1068
  end
781
1069
  return registerHook("postIgnite", _exp, _result_1)
782
1070
  end,
1071
+ onIgnited = function(callback, hookOptions)
1072
+ local _exp = callback
1073
+ local _result_1 = hookOptions
1074
+ if _result_1 ~= nil then
1075
+ _result_1 = _result_1.priority
1076
+ end
1077
+ return registerHook("ignited", _exp, _result_1)
1078
+ end,
783
1079
  onExtinguished = function(callback, hookOptions)
784
1080
  local _exp = callback
785
1081
  local _result_1 = hookOptions
@@ -2,7 +2,7 @@ import { Modding } from "../modding";
2
2
  import { ModuleDefinition, ProviderConfig, type IgniteOptions, type ProviderRegistrationOptions } from "./moduleDefinition";
3
3
  import type { Constructor } from "../utility/constructors";
4
4
  import { type PluginDefinition } from "../plugin/pluginDefinition";
5
- import type { ScopeCondition } from "./scopes";
5
+ import { type ScopeCondition } from "./scopes";
6
6
  type GenericId<T> = string | Modding.Target.Id<T>;
7
7
  export declare class ModuleBuilder {
8
8
  /** A global count of the number of module builders. Used to disambiguate identical module debug names. */
@@ -36,10 +36,16 @@ export declare class ModuleBuilder {
36
36
  /**
37
37
  * Register all providers under the specified path and its descendants.
38
38
  *
39
- * The providers must be exported, and must carry the `@Provider()` decorator themselves: an
40
- * undecorated subclass of a provider is not registered.
39
+ * Every `@Provider()` class the modules there define at their top level is registered, exported
40
+ * or not, as v1 registered every decorated class it required, and so is one they export from
41
+ * elsewhere; each once. A class declared inside a function is registered only if its module
42
+ * exports it. A provider must carry the `@Provider()` decorator itself: an undecorated subclass
43
+ * of a provider is not registered.
41
44
  *
42
45
  * The options apply to every provider found: a scope condition here scopes the whole folder.
46
+ * When it does not hold, the folder is not touched at all -- not looked up, nothing under it
47
+ * required -- so a build can leave the folder out of the place (a release build without its
48
+ * tests, say).
43
49
  *
44
50
  * @metadata macro
45
51
  */
@@ -51,6 +57,9 @@ export declare class ModuleBuilder {
51
57
  * This is the v2 equivalent of v1's `Flamework.addPathsGlob`. Globs can match a large number of
52
58
  * paths, so keep them as specific as possible.
53
59
  *
60
+ * As with `registerProviders`, a scope condition that does not hold leaves every matched folder
61
+ * untouched.
62
+ *
54
63
  * @metadata macro
55
64
  */
56
65
  registerProvidersGlob<T extends string>(_glob: T, options?: ProviderRegistrationOptions, glob?: Modding.Intrinsic<"pathglob", [T], string>): this;
@@ -8,6 +8,8 @@ local LIFECYCLE_SLOT = TS.import(script, script.Parent.Parent, "plugin", "plugin
8
8
  local _providerRegistration = TS.import(script, script.Parent, "providerRegistration")
9
9
  local getProviderClassId = _providerRegistration.getProviderClassId
10
10
  local normalizeProviderConfig = _providerRegistration.normalizeProviderConfig
11
+ local holdsCondition = TS.import(script, script.Parent, "scopes").holdsCondition
12
+ local leftOutRegistration = TS.import(script, script.Parent.Parent, "utility", "leftOut").leftOutRegistration
11
13
  local ModuleBuilder
12
14
  do
13
15
  ModuleBuilder = setmetatable({}, {
@@ -28,6 +30,7 @@ do
28
30
  debugName = "Anonymous",
29
31
  providers = {},
30
32
  plugins = {},
33
+ leftOut = {},
31
34
  }
32
35
  end
33
36
  function ModuleBuilder:includePlugin(plugin, options)
@@ -107,11 +110,30 @@ do
107
110
  function ModuleBuilder:registerProviders(_stringPath, options, path)
108
111
  local _path = path
109
112
  assert(_path)
113
+ -- The active scopes are the ones the build was compiled with, so this is the answer
114
+ -- ignition would give; what it would register is only ever skipped there. Recorded, so that
115
+ -- a miss on a class under the folder can say why it is missing.
116
+ if not holdsCondition(options) then
117
+ local _leftOut = self.module.leftOut
118
+ local _arg0 = leftOutRegistration(`registerProviders("{_stringPath}")`, options, {
119
+ path = path,
120
+ })
121
+ table.insert(_leftOut, _arg0)
122
+ return self
123
+ end
110
124
  return self:registerProviderClasses(getClassesInPath(path), options)
111
125
  end
112
126
  function ModuleBuilder:registerProvidersGlob(_glob, options, glob)
113
127
  local _arg0 = glob ~= nil
114
128
  assert(_arg0)
129
+ if not holdsCondition(options) then
130
+ local _leftOut = self.module.leftOut
131
+ local _arg0_1 = leftOutRegistration(`registerProvidersGlob("{_glob}")`, options, {
132
+ glob = glob,
133
+ })
134
+ table.insert(_leftOut, _arg0_1)
135
+ return self
136
+ end
115
137
  return self:registerProviderClasses(getClassesInGlob(glob), options)
116
138
  end
117
139
  function ModuleBuilder:registerProviderClasses(classes, options)
@@ -2,6 +2,7 @@ import type { Modding } from "../modding";
2
2
  import type { PluginDefinition } from "../plugin/pluginDefinition";
3
3
  import { type Module } from "./module";
4
4
  import type { ScopeCondition } from "./scopes";
5
+ import type { LeftOutRegistration } from "../utility/leftOut";
5
6
  /**
6
7
  * Options for one ignition of a module.
7
8
  *
@@ -44,6 +45,11 @@ export interface ModuleState {
44
45
  readonly providers: readonly ModuleProvider[];
45
46
  /** The plugins to set up on ignition, in inclusion order. */
46
47
  readonly plugins: readonly PluginInclusion[];
48
+ /**
49
+ * The path and glob registrations left out by their own scope condition, whose folders were
50
+ * never looked up: what a miss on a class under one of them names.
51
+ */
52
+ readonly leftOut?: readonly LeftOutRegistration[];
47
53
  }
48
54
  export declare class ModuleDefinition {
49
55
  private moduleState;