@flamework-experimental/core 2.0.0-alpha.0 → 2.0.0-alpha.2

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.
@@ -5,6 +5,9 @@ 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 _threadWaits = TS.import(script, script.Parent.Parent, "utility", "threadWaits")
9
+ local extinguishesBegun = _threadWaits.extinguishesBegun
10
+ local threadWaits = _threadWaits.threadWaits
8
11
  local clearDefaultModule = TS.import(script, script.Parent, "defaultModule").clearDefaultModule
9
12
  local HookPriority = TS.import(script, script.Parent, "moduleHooks").HookPriority
10
13
  local _providerRegistration = TS.import(script, script.Parent, "providerRegistration")
@@ -88,12 +91,24 @@ local function createModuleInstantiation(state, options)
88
91
  local instantiatedProviders = {}
89
92
  --* Objects handed over by `provideInstance`, attached to their interfaces once every plugin is set up.
90
93
  local providedInstances = {}
94
+ --[[
95
+ *
96
+ * Provided objects that have not joined their interfaces: until the ignition reaches them, and
97
+ * for good when an observer refuses one. `release` has nothing to take them out of, and a failed
98
+ * ignition told every observer `onRemoved` for objects it had never been told of, and told the
99
+ * observers that had just undone a refused one of it again.
100
+
101
+ ]]
102
+ local unjoinedInstances = {}
91
103
  local observers = {}
92
104
  local hooks = {}
93
105
  local includedPlugins = {}
94
106
  local filledSlots = {}
95
107
  local temporaryInstances = {}
96
108
  local moduleInitState = ModuleInitState.Created
109
+ --* The thread `extinguish` runs on, and what wakes each thread waiting in `awaitExtinguished` for it to finish.
110
+ local extinguishingThread
111
+ local extinguishWaiters = {}
97
112
  local switchInitState = function(from, to)
98
113
  if moduleInitState ~= from then
99
114
  error(`module '{state.debugName}' is in invalid state when transitioning to '{ModuleInitState[to]}', got '{ModuleInitState[moduleInitState]}' when '{ModuleInitState[from]}' was expected.`)
@@ -292,12 +307,18 @@ local function createModuleInstantiation(state, options)
292
307
  * observer that raises from its `onAdded` has the ones before it told `onRemoved`, and the
293
308
  * error comes out, so a refused object is attached nowhere -- rather than left ticking in the
294
309
  * lifecycle's sets, with no handle to remove it by.
310
+ *
311
+ * `dependencies`, what a provider's constructor was given, is handed to the observers with it.
295
312
 
296
313
  ]]
297
- local registerClassInterfaces = function(instance, kind)
298
- local added = {}
314
+ local registerClassInterfaces = function(instance, kind, dependencies)
315
+ local interfaces = getClassImplements(instance)
316
+ -- How many observers have been told `onAdded`, in the order they were told: what a refusal
317
+ -- undoes. Counted rather than recorded, since an attachment that goes through -- every one,
318
+ -- in a game -- then builds nothing to throw away.
319
+ local attached = 0
299
320
  local success, err = pcall(function()
300
- for _, interfaceId in getClassImplements(instance) do
321
+ for _, interfaceId in interfaces do
301
322
  local interested = observers[interfaceId]
302
323
  if not interested then
303
324
  continue
@@ -308,18 +329,35 @@ local function createModuleInstantiation(state, options)
308
329
  _result_1(instance, {
309
330
  interfaceId = interfaceId,
310
331
  kind = kind,
332
+ dependencies = dependencies,
311
333
  })
312
334
  end
313
- local _arg0 = { interfaceId, observer }
314
- table.insert(added, _arg0)
335
+ attached += 1
315
336
  end
316
337
  end
317
338
  end)
318
339
  if not success then
340
+ -- The observers told, found again by walking the same way as far as the count goes.
341
+ local told = {}
342
+ for _, interfaceId in interfaces do
343
+ local interested = observers[interfaceId]
344
+ if not interested then
345
+ continue
346
+ end
347
+ for _1, observer in interested do
348
+ if #told == attached then
349
+ break
350
+ end
351
+ local _arg0 = { interfaceId, observer }
352
+ table.insert(told, _arg0)
353
+ end
354
+ end
319
355
  -- 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]
356
+ -- the rest attached; the observer's error is what comes out. Marked as refused, so that
357
+ -- an observer owing a departing object a last event -- the lifecycle plugin's
358
+ -- `onExtinguished` while the module extinguishes -- does not deliver it.
359
+ for i = #told - 1, 0, -1 do
360
+ local _binding = told[i + 1]
323
361
  local interfaceId = _binding[1]
324
362
  local observer = _binding[2]
325
363
  guarded("undoing an attachment", function()
@@ -328,6 +366,7 @@ local function createModuleInstantiation(state, options)
328
366
  _result_1 = _result_1(instance, {
329
367
  interfaceId = interfaceId,
330
368
  kind = kind,
369
+ refused = true,
331
370
  })
332
371
  end
333
372
  return _result_1
@@ -353,11 +392,15 @@ local function createModuleInstantiation(state, options)
353
392
  end
354
393
  end
355
394
  end
395
+ --* Constructs a class, resolving what its constructor takes into `resolvedParameters`.
356
396
  local resolveDependencyWithOrigin
357
- local instantiateClassWithDependencies = function(constructor, resolve)
397
+ local instantiateClassWithDependencies = function(constructor, resolve, resolvedParameters)
398
+ if resolvedParameters == nil then
399
+ resolvedParameters = {}
400
+ end
358
401
  local dependencies = Reflect.getMetadata(constructor, "flamework:dependencies") or {}
359
- local resolvedParameters = {}
360
402
  for _, dependency in dependencies do
403
+ local _resolvedParameters = resolvedParameters
361
404
  local _result_1 = resolve
362
405
  if _result_1 ~= nil then
363
406
  _result_1 = _result_1(dependency)
@@ -366,7 +409,7 @@ local function createModuleInstantiation(state, options)
366
409
  if _condition_1 == nil then
367
410
  _condition_1 = resolveDependencyWithOrigin(dependency, constructor)
368
411
  end
369
- table.insert(resolvedParameters, _condition_1)
412
+ table.insert(_resolvedParameters, _condition_1)
370
413
  end
371
414
  return constructor.new(unpack(resolvedParameters))
372
415
  end
@@ -405,10 +448,21 @@ local function createModuleInstantiation(state, options)
405
448
  if moduleProvider then
406
449
  local config = moduleProvider.config
407
450
  if config.type == "class" then
408
- local instantiatedProvider = instantiateClassWithDependencies(config.value)
451
+ local dependencies = {}
452
+ local instantiatedProvider = instantiateClassWithDependencies(config.value, nil, dependencies)
409
453
  local _id_1 = info.id
410
454
  instantiatedProviders[_id_1] = instantiatedProvider
411
- registerClassInterfaces(instantiatedProvider, "provider")
455
+ -- Held only if it is attached: one an observer refuses is attached nowhere, so it must
456
+ -- not be cached either -- the next resolve handed it out with no lifecycle at all, and
457
+ -- `release` told every observer, the refusing one included, it was removed again.
458
+ local attached, err = pcall(function()
459
+ return registerClassInterfaces(instantiatedProvider, "provider", dependencies)
460
+ end)
461
+ if not attached then
462
+ local _id_2 = info.id
463
+ instantiatedProviders[_id_2] = nil
464
+ error(err, 0)
465
+ end
412
466
  return instantiatedProvider
413
467
  elseif config.type == "function" then
414
468
  -- Function providers are not cached.
@@ -602,6 +656,7 @@ local function createModuleInstantiation(state, options)
602
656
  -- every plugin's observers are in place.
603
657
  for _, instance in providedInstances do
604
658
  registerClassInterfaces(instance, "provider")
659
+ unjoinedInstances[instance] = nil
605
660
  end
606
661
  for _, provider in providers do
607
662
  -- Lazy providers are constructed the first time they are resolved instead.
@@ -610,6 +665,15 @@ local function createModuleInstantiation(state, options)
610
665
  end
611
666
  end
612
667
  runHooks("postIgnite")
668
+ -- Checked again once everything has run: an `onInit` that yields lets other threads run in
669
+ -- the middle of ignition, and one that extinguished an import then did not see this module
670
+ -- among its importers, since it joins them only below. Carrying on left it ignited onto a
671
+ -- released import that would never take it down.
672
+ for _, imported in imports do
673
+ if not imported.isIgnited() then
674
+ error(`module '{state.debugName}': imported module '{imported.debugName}' was extinguished while this module was igniting`)
675
+ end
676
+ end
613
677
  end)
614
678
  if not success then
615
679
  moduleInitState = ModuleInitState.Extinguishing
@@ -621,12 +685,29 @@ local function createModuleInstantiation(state, options)
621
685
  for _, imported in imports do
622
686
  imported.addImporter(module)
623
687
  end
688
+ -- Ignition has completed, so this is where the lifecycle plugin starts the providers: an
689
+ -- `onStart` sees the module ignited, may extinguish it, and may ignite one that imports it.
690
+ -- Nothing here can fail the ignition, so a hook that raises is warned about. One that
691
+ -- extinguishes the module ends the phase: the hooks after it would set up a module gone.
692
+ for _, hook in sortedHooks("ignited") do
693
+ if moduleInitState ~= ModuleInitState.Ignited then
694
+ break
695
+ end
696
+ guarded("an onIgnited hook", function()
697
+ return hook.callback(module)
698
+ end)
699
+ end
624
700
  return module
625
701
  end
626
702
  local extinguish = function()
627
703
  switchInitState(ModuleInitState.Ignited, ModuleInitState.Extinguishing)
704
+ extinguishingThread = coroutine.running()
705
+ extinguishesBegun.count += 1
628
706
  -- 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.
707
+ -- down before it returns: the deepest goes first. Copied, since each removes itself. One
708
+ -- extinguishing already, on another thread suspended in a handler that yields, is still
709
+ -- using this module's providers, so it is waited for: skipped, it had this module released
710
+ -- under it.
630
711
  local _array_2 = {}
631
712
  local _length_1 = #_array_2
632
713
  for _v in importers do
@@ -636,6 +717,8 @@ local function createModuleInstantiation(state, options)
636
717
  for _, importer in _array_2 do
637
718
  if not importer.isExtinguished() then
638
719
  importer.extinguish()
720
+ else
721
+ importer.awaitExtinguished()
639
722
  end
640
723
  end
641
724
  table.clear(importers)
@@ -674,10 +757,31 @@ local function createModuleInstantiation(state, options)
674
757
  -- Providers join their interfaces when they are instantiated, so they have to leave them
675
758
  -- too. Without this, a plugin such as the lifecycle plugin keeps holding (and ticking)
676
759
  -- 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)
760
+ --
761
+ -- Over a copy, taken again until nothing new turns up: a removal runs user code -- the
762
+ -- lifecycle plugin tells a provider resolved since its walk `onExtinguished` as it leaves --
763
+ -- which may resolve a lazy provider for the first time, and a table gaining keys while it
764
+ -- is walked skipped providers, or had others unregistered twice. By object, not by id: an
765
+ -- object provided under several ids joined its interfaces once, so it leaves them once.
766
+ local released = {}
767
+ while true do
768
+ local remaining = {}
769
+ for _, provider in instantiatedProviders do
770
+ if not (released[provider] ~= nil) then
771
+ released[provider] = true
772
+ if not (unjoinedInstances[provider] ~= nil) then
773
+ table.insert(remaining, provider)
774
+ end
775
+ end
776
+ end
777
+ if #remaining == 0 then
778
+ break
779
+ end
780
+ for _, provider in remaining do
781
+ guarded("removing a provider", function()
782
+ return unregisterClassInterfaces(provider, "provider")
783
+ end)
784
+ end
681
785
  end
682
786
  -- A removal that raised leaves its instance behind, and nothing can add one any more.
683
787
  table.clear(temporaryInstances)
@@ -689,6 +793,80 @@ local function createModuleInstantiation(state, options)
689
793
  -- Released last, once nothing in here can resolve any more, so that the next root ignited
690
794
  -- becomes the default rather than `Dependency<T>()` answering from a dead module.
691
795
  clearDefaultModule(module)
796
+ for _, wake in extinguishWaiters do
797
+ wake()
798
+ end
799
+ table.clear(extinguishWaiters)
800
+ end
801
+ local awaitExtinguished = function()
802
+ if moduleInitState ~= ModuleInitState.Extinguishing then
803
+ return nil
804
+ end
805
+ -- On the extinguish's own thread -- one of its handlers extinguishing an import -- it cannot
806
+ -- finish until this returns, so waiting would never end. Nor on a thread the extinguish's
807
+ -- thread is itself waiting on here, however many waits away: two extinguishes, each waiting
808
+ -- for a module the other is taking down, held each other -- and every module between them,
809
+ -- still ticking -- for good. The wait that would close the circle is skipped, as the wait on
810
+ -- the extinguish's own thread is. The chain runs through an ignition waiting for an `onInit`
811
+ -- on its own thread too, which the lifecycle plugin records -- and through one waiting for the
812
+ -- Promise an `onInit` returned, whose work runs on a thread nothing can name: an `async`
813
+ -- method's body runs on one of its own. A chain that ends at one is taken to close the circle,
814
+ -- since the thread that settles it may be this one.
815
+ local running = coroutine.running()
816
+ local thread = extinguishingThread
817
+ local closesCircle = function()
818
+ local waitedOn = thread
819
+ while waitedOn ~= nil do
820
+ local _condition_1 = waitedOn == running
821
+ if not _condition_1 then
822
+ local _waitedOn = waitedOn
823
+ _condition_1 = not (type(_waitedOn) == "thread")
824
+ end
825
+ if _condition_1 then
826
+ return true
827
+ end
828
+ local _waitedOn = waitedOn
829
+ waitedOn = threadWaits[_waitedOn]
830
+ end
831
+ return false
832
+ end
833
+ if closesCircle() then
834
+ return nil
835
+ end
836
+ -- Nor on a thread that has died -- cancelled, as the testing runner cancels a body that
837
+ -- overran, or closed: that extinguish never finishes, and waiting for it held this module,
838
+ -- and everything in it, for good. One that dies while this waits is noticed within a frame.
839
+ if coroutine.status(thread) == "dead" then
840
+ return nil
841
+ end
842
+ local woken = false
843
+ local wake = function()
844
+ if woken then
845
+ return nil
846
+ end
847
+ woken = true
848
+ -- Removed here rather than by this thread once it resumes: one cancelled while it waits
849
+ -- never does, and left its entry behind for good.
850
+ threadWaits[running] = nil
851
+ if coroutine.status(running) ~= "dead" then
852
+ task.spawn(running)
853
+ end
854
+ end
855
+ threadWaits[running] = thread
856
+ table.insert(extinguishWaiters, wake)
857
+ -- Checked again every frame, as well as for a dead extinguish: this thread may be cancelled
858
+ -- while it waits, and the chain may come to close the circle after the wait began -- an
859
+ -- `async` `onInit`'s body waits here before the ignition that called it takes the Promise it
860
+ -- returned and waits on that.
861
+ task.spawn(function()
862
+ while not woken do
863
+ task.wait()
864
+ if coroutine.status(thread) == "dead" or coroutine.status(running) == "dead" or closesCircle() then
865
+ wake()
866
+ end
867
+ end
868
+ end)
869
+ coroutine.yield()
692
870
  end
693
871
  local isExtinguished = function()
694
872
  return moduleInitState >= ModuleInitState.Extinguishing
@@ -721,6 +899,7 @@ local function createModuleInstantiation(state, options)
721
899
  -- ▲ Set.delete ▲
722
900
  return _valueExisted
723
901
  end,
902
+ awaitExtinguished = awaitExtinguished,
724
903
  }
725
904
  --* What a plugin's setup is handed. Everything registers into this instantiation.
726
905
  pluginTarget = {
@@ -760,8 +939,15 @@ local function createModuleInstantiation(state, options)
760
939
  local _injectionId = injectionId
761
940
  local _instance = instance
762
941
  instantiatedProviders[_injectionId] = _instance
942
+ -- Once, however many ids it is provided under: it joins its interfaces once per entry,
943
+ -- and the same object twice was initialised, started and told `onAdded` twice.
763
944
  local _instance_1 = instance
764
- table.insert(providedInstances, _instance_1)
945
+ if not (table.find(providedInstances, _instance_1) ~= nil) then
946
+ local _instance_2 = instance
947
+ table.insert(providedInstances, _instance_2)
948
+ local _instance_3 = instance
949
+ unjoinedInstances[_instance_3] = true
950
+ end
765
951
  end,
766
952
  includePlugin = includePlugin,
767
953
  onPreIgnite = function(callback, hookOptions)
@@ -780,6 +966,14 @@ local function createModuleInstantiation(state, options)
780
966
  end
781
967
  return registerHook("postIgnite", _exp, _result_1)
782
968
  end,
969
+ onIgnited = function(callback, hookOptions)
970
+ local _exp = callback
971
+ local _result_1 = hookOptions
972
+ if _result_1 ~= nil then
973
+ _result_1 = _result_1.priority
974
+ end
975
+ return registerHook("ignited", _exp, _result_1)
976
+ end,
783
977
  onExtinguished = function(callback, hookOptions)
784
978
  local _exp = callback
785
979
  local _result_1 = hookOptions
@@ -94,6 +94,13 @@ export interface PluginTarget {
94
94
  onPreIgnite: (callback: (module: Module) => void, options?: HookOptions) => void;
95
95
  /** Runs after every provider has been constructed. */
96
96
  onPostIgnite: (callback: (module: Module) => void, options?: HookOptions) => void;
97
+ /**
98
+ * Runs once ignition has completed: the module is ignited, and its imports count it among
99
+ * their importers. The lifecycle plugin starts the providers here. Nothing can fail the
100
+ * ignition any more, so a hook that raises is warned about and the ones after it still run;
101
+ * once the module has been extinguished -- by an `onStart`, say -- the rest do not run.
102
+ */
103
+ onIgnited: (callback: (module: Module) => void, options?: HookOptions) => void;
97
104
  /** Runs when the module extinguishes, before its providers are released. */
98
105
  onExtinguished: (callback: (module: Module) => void, options?: HookOptions) => void;
99
106
  /**
package/out/reflect.luau CHANGED
@@ -1,4 +1,6 @@
1
1
  -- Compiled with roblox-ts v3.0.0
2
+ local TS = _G[script]
3
+ local forgetImplements = TS.import(script, script.Parent, "utility", "implementsCache").forgetImplements
2
4
  --[[
3
5
  *
4
6
  * Reflection/metadata API
@@ -60,6 +62,10 @@ do
60
62
  local _key = key
61
63
  local _value = value
62
64
  metadata[_key] = _value
65
+ -- What `getClassImplements` keeps per class is built from this key.
66
+ if key == "flamework:implements" then
67
+ forgetImplements(obj)
68
+ end
63
69
  end
64
70
  _container.defineMetadata = defineMetadata
65
71
  --[[
@@ -72,6 +78,7 @@ do
72
78
  for key, value in pairs(list) do
73
79
  metadata[key] = value
74
80
  end
81
+ forgetImplements(obj)
75
82
  end
76
83
  _container.defineMetadataBatch = defineMetadataBatch
77
84
  --[[
@@ -86,6 +93,9 @@ do
86
93
  local _key = key
87
94
  _result[_key] = nil
88
95
  end
96
+ if key == "flamework:implements" then
97
+ forgetImplements(obj)
98
+ end
89
99
  end
90
100
  _container.deleteMetadata = deleteMetadata
91
101
  --[[
@@ -302,6 +312,7 @@ do
302
312
  local function resetObject(object)
303
313
  local _object = object
304
314
  metadata[_object] = nil
315
+ forgetImplements(object)
305
316
  end
306
317
  _container.resetObject = resetObject
307
318
  end
@@ -1,7 +1,14 @@
1
1
  /**
2
- * The interfaces a class implements, own and inherited, each once: the transformer writes every
2
+ * The interfaces an object implements, own and inherited, each once: the transformer writes every
3
3
  * class's own heritage clause, so a subclass that re-declares an interface its parent implements
4
4
  * carries the id twice up the chain, and attached to it twice -- the lifecycle's ordered lists ran
5
5
  * `onInit` and `onStart` twice for it.
6
+ *
7
+ * Walked once per class and kept: this runs on every attach and detach, and behind
8
+ * `Flamework.implements`. An instance, which implements nothing of its own, answers its class's
9
+ * list; so does a class asked directly. An object that implements something of its own -- a
10
+ * `listen` proxy, a plain object given the metadata -- or whose `__index` is not a table is walked
11
+ * every time, as before, rather than kept per object. The list is shared and frozen: callers must
12
+ * not change it.
6
13
  */
7
- export declare function getClassImplements(constructor: object): string[];
14
+ export declare function getClassImplements(object: object): ReadonlyArray<string>;
@@ -1,18 +1,26 @@
1
1
  -- Compiled with roblox-ts v3.0.0
2
2
  local TS = _G[script]
3
3
  local Reflect = TS.import(script, script.Parent.Parent, "reflect").Reflect
4
+ local implementsCache = TS.import(script, script.Parent, "implementsCache").implementsCache
5
+ local IMPLEMENTS = "flamework:implements"
6
+ local EMPTY = table.freeze({})
7
+ --* Where `Reflect.getMetadatas` goes next from an object: its metatable's `__index`, its class.
8
+ local function getParent(object)
9
+ local metatable = getmetatable(object)
10
+ if metatable ~= nil and type(metatable) == "table" then
11
+ return rawget(metatable, "__index")
12
+ end
13
+ end
4
14
  --[[
5
15
  *
6
- * The interfaces a class implements, own and inherited, each once: the transformer writes every
7
- * class's own heritage clause, so a subclass that re-declares an interface its parent implements
8
- * carries the id twice up the chain, and attached to it twice -- the lifecycle's ordered lists ran
9
- * `onInit` and `onStart` twice for it.
16
+ * The walk as `Reflect.getMetadatas` makes it, for an object whose list is not cached: every
17
+ * `flamework:implements` list from the object up its chain, each id once, first seen first.
10
18
 
11
19
  ]]
12
- local function getClassImplements(constructor)
20
+ local function collect(object)
13
21
  local classImplements = {}
14
22
  local seen = {}
15
- for _, implementList in Reflect.getMetadatas(constructor, "flamework:implements") do
23
+ for _, implementList in Reflect.getMetadatas(object, IMPLEMENTS) do
16
24
  for _1, implementId in implementList do
17
25
  if not (seen[implementId] ~= nil) then
18
26
  seen[implementId] = true
@@ -22,6 +30,81 @@ local function getClassImplements(constructor)
22
30
  end
23
31
  return classImplements
24
32
  end
33
+ --[[
34
+ *
35
+ * A class's list, built once: its own ids, then those of the class above it that it does not
36
+ * re-declare, which is the order and the ids `collect` finds. One that declares nothing of its own
37
+ * shares the list above it.
38
+
39
+ ]]
40
+ local function getCachedImplements(object)
41
+ local _byClass = implementsCache.byClass
42
+ local _object = object
43
+ local cached = _byClass[_object]
44
+ if cached ~= nil then
45
+ return cached
46
+ end
47
+ local parent = getParent(object)
48
+ local inherited = if parent ~= nil then getCachedImplements(parent) else EMPTY
49
+ local list = inherited
50
+ local own = Reflect.getOwnMetadata(object, IMPLEMENTS)
51
+ if own ~= nil then
52
+ local merged = {}
53
+ for _, implementId in own do
54
+ if not (table.find(merged, implementId) ~= nil) then
55
+ table.insert(merged, implementId)
56
+ end
57
+ end
58
+ for _, implementId in inherited do
59
+ if not (table.find(merged, implementId) ~= nil) then
60
+ table.insert(merged, implementId)
61
+ end
62
+ end
63
+ list = table.freeze(merged)
64
+ end
65
+ local _byClass_1 = implementsCache.byClass
66
+ local _object_1 = object
67
+ local _list = list
68
+ _byClass_1[_object_1] = _list
69
+ return list
70
+ end
71
+ --[[
72
+ *
73
+ * The interfaces an object implements, own and inherited, each once: the transformer writes every
74
+ * class's own heritage clause, so a subclass that re-declares an interface its parent implements
75
+ * carries the id twice up the chain, and attached to it twice -- the lifecycle's ordered lists ran
76
+ * `onInit` and `onStart` twice for it.
77
+ *
78
+ * Walked once per class and kept: this runs on every attach and detach, and behind
79
+ * `Flamework.implements`. An instance, which implements nothing of its own, answers its class's
80
+ * list; so does a class asked directly. An object that implements something of its own -- a
81
+ * `listen` proxy, a plain object given the metadata -- or whose `__index` is not a table is walked
82
+ * every time, as before, rather than kept per object. The list is shared and frozen: callers must
83
+ * not change it.
84
+
85
+ ]]
86
+ local function getClassImplements(object)
87
+ if Reflect.getOwnMetadata(object, IMPLEMENTS) == nil then
88
+ local parent = getParent(object)
89
+ if parent == nil then
90
+ return EMPTY
91
+ end
92
+ if type(parent) == "table" then
93
+ return getCachedImplements(parent)
94
+ end
95
+ else
96
+ local _object = object
97
+ local _condition = type(_object) == "table"
98
+ if _condition then
99
+ _condition = rawget(object, "__index") == object
100
+ end
101
+ if _condition then
102
+ -- A class: a roblox-ts class is its own instances' `__index`.
103
+ return getCachedImplements(object)
104
+ end
105
+ end
106
+ return collect(object)
107
+ end
25
108
  return {
26
109
  getClassImplements = getClassImplements,
27
110
  }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * What each class implements, own and inherited, as `getClassImplements` found it, by the class --
3
+ * and by every class above it, which it walked on the way. Its own module, apart from
4
+ * `getClassImplements`, so that `Reflect`, which that one reads through, can reach it too.
5
+ */
6
+ export declare const implementsCache: {
7
+ byClass: WeakMap<object, readonly string[]>;
8
+ };
9
+ /**
10
+ * Forgets every list once an object whose `flamework:implements` metadata one was built from has
11
+ * it changed: the lists of the classes below it were built from it too. The transformer writes a
12
+ * class's metadata as the class is defined, before anything can ask, so this does not happen in a
13
+ * game; it keeps a later change seen as it was before the lists were cached. What `listen` gives
14
+ * its proxies changes nothing here: an object that implements something of its own is not cached.
15
+ */
16
+ export declare function forgetImplements(object: object): void;
@@ -0,0 +1,35 @@
1
+ -- Compiled with roblox-ts v3.0.0
2
+ --[[
3
+ *
4
+ * What each class implements, own and inherited, as `getClassImplements` found it, by the class --
5
+ * and by every class above it, which it walked on the way. Its own module, apart from
6
+ * `getClassImplements`, so that `Reflect`, which that one reads through, can reach it too.
7
+
8
+ ]]
9
+ local implementsCache = {
10
+ byClass = setmetatable({}, {
11
+ __mode = "k",
12
+ }),
13
+ }
14
+ --[[
15
+ *
16
+ * Forgets every list once an object whose `flamework:implements` metadata one was built from has
17
+ * it changed: the lists of the classes below it were built from it too. The transformer writes a
18
+ * class's metadata as the class is defined, before anything can ask, so this does not happen in a
19
+ * game; it keeps a later change seen as it was before the lists were cached. What `listen` gives
20
+ * its proxies changes nothing here: an object that implements something of its own is not cached.
21
+
22
+ ]]
23
+ local function forgetImplements(object)
24
+ local _byClass = implementsCache.byClass
25
+ local _object = object
26
+ if _byClass[_object] ~= nil then
27
+ implementsCache.byClass = setmetatable({}, {
28
+ __mode = "k",
29
+ })
30
+ end
31
+ end
32
+ return {
33
+ forgetImplements = forgetImplements,
34
+ implementsCache = implementsCache,
35
+ }
@@ -1 +1,13 @@
1
+ /**
2
+ * Runs `func` with the arguments given, on a thread that is kept for the next call once it returns:
3
+ * one that yields keeps its thread, and the next call gets a new one.
4
+ *
5
+ * The arguments are passed through rather than closed over, so that a caller running many callbacks
6
+ * -- the per-frame events, once per listener every frame -- creates nothing per call.
7
+ */
1
8
  export declare function recycleThread(func: () => void): void;
9
+ export declare function recycleThread<A>(func: (a: A) => void, a: A): void;
10
+ export declare function recycleThread<A, B>(func: (a: A, b: B) => void, a: A, b: B): void;
11
+ export declare function recycleThread<A, B, C>(func: (a: A, b: B, c: C) => void, a: A, b: B, c: C): void;
12
+ export declare function recycleThread<A, B, C, D>(func: (a: A, b: B, c: C, d: D) => void, a: A, b: B, c: C, d: D): void;
13
+ export declare function recycleThread<A, B, C, D, E>(func: (a: A, b: B, c: C, d: D, e: E) => void, a: A, b: B, c: C, d: D, e: E): void;