@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,10 +5,121 @@ local RunService = TS.import(script, TS.getModule(script, "@rbxts", "services"))
5
5
  local getRuntimeConfig = TS.import(script, script.Parent.Parent, "utility", "runtimeConfig").getRuntimeConfig
6
6
  local Provider = TS.import(script, script.Parent.Parent, "provider").Provider
7
7
  local recycleThread = TS.import(script, script.Parent.Parent, "utility", "recycleThread").recycleThread
8
+ local _threadWaits = TS.import(script, script.Parent.Parent, "utility", "threadWaits")
9
+ local extinguishesBegun = _threadWaits.extinguishesBegun
10
+ local runsPromiseWork = _threadWaits.runsPromiseWork
11
+ local threadWaits = _threadWaits.threadWaits
8
12
  local Reflect = TS.import(script, script.Parent.Parent, "reflect").Reflect
13
+ local DEFAULT_LOAD_ORDER = TS.import(script, script.Parent.Parent, "module", "providerRegistration").DEFAULT_LOAD_ORDER
9
14
  local _pluginDefinition = TS.import(script, script.Parent.Parent, "plugin", "pluginDefinition")
10
15
  local LIFECYCLE_SLOT = _pluginDefinition.LIFECYCLE_SLOT
11
16
  local PluginDefinition = _pluginDefinition.PluginDefinition
17
+ --[[
18
+ *
19
+ * Late providers whose `onInit` has yet to finish, or to raise, to the plugin that runs it: what a
20
+ * dependent's waits for. Shared by every module's plugin, since a provider's constructor may take a
21
+ * lazy provider of an import, which the import's plugin initialises.
22
+
23
+ ]]
24
+ local pendingInits = {}
25
+ --[[
26
+ *
27
+ * How many seconds a dependency wait that may be on itself (see `mayWaitForRunningThread`) lasts
28
+ * before it is warned about.
29
+
30
+ ]]
31
+ local SELF_WAIT_WARNING = 5
32
+ --[[
33
+ *
34
+ * A per-frame method read off its object as a plain function, and called with the object as `self`:
35
+ * roblox-ts refuses a reference to a method that does not call it, and calling it through the object
36
+ * took a closure per listener per frame to hand to the thread that runs it.
37
+
38
+ ]]
39
+ --* The per-frame methods, by name, as `FrameCallback`s.
40
+ --[[
41
+ *
42
+ * Runs a per-frame callback under the MicroProfiler label and the memory category of its object's
43
+ * identifier. A function of its own, handed its arguments, rather than a closure per call.
44
+
45
+ ]]
46
+ local function runProfiled(id, callback, object, dt, now)
47
+ -- `profilebegin` ends when the thread yields or dies.
48
+ debug.profilebegin(id)
49
+ debug.setmemorycategory(id)
50
+ callback(object, dt, now)
51
+ debug.resetmemorycategory()
52
+ end
53
+ --[[
54
+ *
55
+ * The listeners of one per-frame event, walked in place every frame rather than over a copy.
56
+ *
57
+ * Luau lets a walk clear keys of the table it walks, ones it has yet to reach included -- which it
58
+ * then passes over -- but not add them: a key added while a table is walked can make the walk visit
59
+ * others twice, or not at all, once the table grows. So what is detached leaves `members` at once,
60
+ * and is not called if its turn this frame has yet to come, while what is attached during the
61
+ * event's walk -- by a callback: `createClassInstance`, `listen`, a lazy provider resolved -- waits
62
+ * in `added` and joins once the walk is over, for its first call on the next frame.
63
+
64
+ ]]
65
+ local FrameListeners
66
+ do
67
+ FrameListeners = setmetatable({}, {
68
+ __tostring = function()
69
+ return "FrameListeners"
70
+ end,
71
+ })
72
+ FrameListeners.__index = FrameListeners
73
+ function FrameListeners.new(...)
74
+ local self = setmetatable({}, FrameListeners)
75
+ return self:constructor(...) or self
76
+ end
77
+ function FrameListeners:constructor(members)
78
+ self.members = members
79
+ self.walking = false
80
+ self.added = {}
81
+ end
82
+ function FrameListeners:has(value)
83
+ local _members = self.members
84
+ local _value = value
85
+ local _condition = _members[_value] ~= nil
86
+ if not _condition then
87
+ local _added = self.added
88
+ local _value_1 = value
89
+ _condition = _added[_value_1] ~= nil
90
+ end
91
+ return _condition
92
+ end
93
+ function FrameListeners:add(value)
94
+ if self.walking then
95
+ local _added = self.added
96
+ local _value = value
97
+ _added[_value] = true
98
+ else
99
+ local _members = self.members
100
+ local _value = value
101
+ _members[_value] = true
102
+ end
103
+ end
104
+ function FrameListeners:delete(value)
105
+ local _members = self.members
106
+ local _value = value
107
+ _members[_value] = nil
108
+ local _added = self.added
109
+ local _value_1 = value
110
+ _added[_value_1] = nil
111
+ end
112
+ function FrameListeners:endWalk()
113
+ self.walking = false
114
+ if next(self.added) == nil then
115
+ return nil
116
+ end
117
+ for value in self.added do
118
+ self.members[value] = true
119
+ end
120
+ table.clear(self.added)
121
+ end
122
+ end
12
123
  --[[
13
124
  *
14
125
  * Tracks the objects attached to each lifecycle event for one module.
@@ -34,14 +145,26 @@ do
34
145
  self.onInit = {}
35
146
  self.initMembers = {}
36
147
  self.startOrder = {}
148
+ self.startLoadOrders = {}
37
149
  self.onStart = {}
38
150
  self.onTick = {}
39
151
  self.onPhysics = {}
40
152
  self.onRender = {}
41
153
  self.onExtinguished = {}
154
+ self.tickListeners = FrameListeners.new(self.onTick)
155
+ self.physicsListeners = FrameListeners.new(self.onPhysics)
156
+ self.renderListeners = FrameListeners.new(self.onRender)
42
157
  self.identifiers = {}
43
158
  self.moduleConnections = {}
44
159
  self.lateProviders = {}
160
+ self.lateQueue = {}
161
+ self.hasLateTurn = false
162
+ self.lateTurns = {}
163
+ self.lateTurnInits = {}
164
+ self.heldLateProviders = {}
165
+ self.initDependencies = {}
166
+ self.untold = {}
167
+ self.isExtinguishing = false
45
168
  self.hasStarted = false
46
169
  -- Per-module option, then the project's flamework.config.json, then Studio.
47
170
  local _condition = options.profiling
@@ -83,23 +206,11 @@ do
83
206
  local _object_1 = object
84
207
  _condition = _onStart[_object_1] ~= nil
85
208
  if not _condition then
86
- local _onTick = self.onTick
87
- local _object_2 = object
88
- _condition = _onTick[_object_2] ~= nil
209
+ _condition = self.tickListeners:has(object) or self.physicsListeners:has(object) or self.renderListeners:has(object)
89
210
  if not _condition then
90
- local _onPhysics = self.onPhysics
91
- local _object_3 = object
92
- _condition = _onPhysics[_object_3] ~= nil
93
- if not _condition then
94
- local _onRender = self.onRender
95
- local _object_4 = object
96
- _condition = _onRender[_object_4] ~= nil
97
- if not _condition then
98
- local _onExtinguished = self.onExtinguished
99
- local _object_5 = object
100
- _condition = _onExtinguished[_object_5] ~= nil
101
- end
102
- end
211
+ local _onExtinguished = self.onExtinguished
212
+ local _object_2 = object
213
+ _condition = _onExtinguished[_object_2] ~= nil
103
214
  end
104
215
  end
105
216
  end
@@ -108,6 +219,12 @@ do
108
219
  local _identifiers = self.identifiers
109
220
  local _object_1 = object
110
221
  _identifiers[_object_1] = nil
222
+ local _lateProviders = self.lateProviders
223
+ local _object_2 = object
224
+ _lateProviders[_object_2] = nil
225
+ local _initDependencies = self.initDependencies
226
+ local _object_3 = object
227
+ _initDependencies[_object_3] = nil
111
228
  end
112
229
  end
113
230
  function LifecycleProvider:removeFrom(set, value)
@@ -116,34 +233,125 @@ do
116
233
  _set[_value] = nil
117
234
  self:forget(value)
118
235
  end
119
- function LifecycleProvider:profile(callback, object)
120
- if self.isProfiling then
121
- local id = self:getIdentifier(object)
122
- return recycleThread(function()
123
- -- `profilebegin` ends when the thread yields or dies.
124
- debug.profilebegin(id)
125
- debug.setmemorycategory(id)
126
- callback()
127
- debug.resetmemorycategory()
128
- end)
236
+ function LifecycleProvider:observeFrameEvents(target)
237
+ target.observe(self:observeFrameEvent(self.tickListeners), "$:lifecycle/lifecycleInterfaces@OnTick")
238
+ target.observe(self:observeFrameEvent(self.renderListeners), "$:lifecycle/lifecycleInterfaces@OnRender")
239
+ target.observe(self:observeFrameEvent(self.physicsListeners), "$:lifecycle/lifecycleInterfaces@OnPhysics")
240
+ end
241
+ function LifecycleProvider:observeFrameEvent(listeners)
242
+ return {
243
+ onAdded = function(value, context)
244
+ listeners:add(value)
245
+ if context.kind == "provider" then
246
+ self:addFrameProvider(value, context)
247
+ end
248
+ end,
249
+ onRemoved = function(value)
250
+ listeners:delete(value)
251
+ self:forget(value)
252
+ end,
253
+ }
254
+ end
255
+ function LifecycleProvider:addFrameProvider(object, context)
256
+ if self:recordDependencies(object, context) and self.hasStarted then
257
+ self:scheduleLateProvider(object)
129
258
  end
130
- return recycleThread(callback)
259
+ end
260
+ function LifecycleProvider:walkFrame(listeners, method, dt, now)
261
+ if self:hasBegunExtinguishing() then
262
+ return nil
263
+ end
264
+ local late = if next(self.lateProviders) == nil then nil else self.lateProviders
265
+ local profiling = self.isProfiling
266
+ local extinguishes = extinguishesBegun.count
267
+ listeners.walking = true
268
+ for listener in listeners.members do
269
+ if late ~= nil and late[listener] ~= nil then
270
+ continue
271
+ end
272
+ local callback = listener[method]
273
+ if profiling then
274
+ recycleThread(runProfiled, self:getIdentifier(listener), callback, listener, dt, now)
275
+ else
276
+ recycleThread(callback, listener, dt, now)
277
+ end
278
+ if extinguishesBegun.count ~= extinguishes then
279
+ extinguishes = extinguishesBegun.count
280
+ if self:hasBegunExtinguishing() then
281
+ break
282
+ end
283
+ end
284
+ end
285
+ listeners:endWalk()
131
286
  end
132
287
  function LifecycleProvider:runInit(object)
133
288
  local id = self:getIdentifier(object)
134
- if self.isProfiling then
135
- debug.setmemorycategory(id)
136
- end
137
- local result = object:onInit()
289
+ local result = self:callInit(object, id)
138
290
  if TS.Promise.is(result) then
291
+ -- Recorded as a wait on the Promise, as `callInit` records one on its thread: an `async`
292
+ -- `onInit` runs its body on a thread of its own, so one that extinguished an import of a
293
+ -- module this thread was extinguishing waited for this thread, which waited here for its
294
+ -- Promise, for good. Removed as the Promise settles, since a cancelled caller never resumes.
295
+ if result:getStatus() == TS.Promise.Status.Started then
296
+ local caller = coroutine.running()
297
+ threadWaits[caller] = result
298
+ result:finally(function()
299
+ threadWaits[caller] = nil
300
+ end):catch(function() end)
301
+ end
139
302
  local status, value = result:awaitStatus()
140
303
  if status == TS.Promise.Status.Rejected then
141
304
  error(`onInit failed for '{id}': {tostring(value)}`, 0)
142
305
  end
143
306
  end
144
- if self.isProfiling then
145
- debug.resetmemorycategory()
307
+ end
308
+ function LifecycleProvider:callInit(object, id)
309
+ local caller = coroutine.running()
310
+ local outcome = nil
311
+ local waiting = false
312
+ local initThread = coroutine.create(function()
313
+ if self.isProfiling then
314
+ debug.setmemorycategory(id)
315
+ end
316
+ local success, value = pcall(function()
317
+ return object:onInit()
318
+ end)
319
+ outcome = { success, value }
320
+ -- Removed here rather than by the caller once it resumes: a caller cancelled meanwhile --
321
+ -- the testing runner cancels a body that overran -- never does, and left its entry behind.
322
+ threadWaits[caller] = nil
323
+ if waiting then
324
+ task.spawn(caller)
325
+ end
326
+ end)
327
+ -- Recorded as a wait, from before it starts, so that an extinguish can tell the caller waits
328
+ -- on it: an `onInit` that extinguished an import of a module the caller was extinguishing --
329
+ -- an ignition started from that module's `onExtinguished` -- waited for the caller, which
330
+ -- waited here, for good. On the caller's own thread, it would have gone straight through.
331
+ threadWaits[caller] = initThread
332
+ task.spawn(initThread)
333
+ if outcome == nil then
334
+ waiting = true
335
+ coroutine.yield()
336
+ end
337
+ local _binding = outcome
338
+ local success = _binding[1]
339
+ local value = _binding[2]
340
+ if not success then
341
+ error(value, 0)
146
342
  end
343
+ return value
344
+ end
345
+ function LifecycleProvider:hasBegunExtinguishing()
346
+ local _condition = self.isExtinguishing
347
+ if not _condition then
348
+ local _result = self.module
349
+ if _result ~= nil then
350
+ _result = _result.isExtinguished()
351
+ end
352
+ _condition = _result == true
353
+ end
354
+ return _condition
147
355
  end
148
356
  function LifecycleProvider:runStart(object)
149
357
  task.spawn(function()
@@ -159,26 +367,218 @@ do
159
367
  local _lateProviders_1 = self.lateProviders
160
368
  local _object_1 = object
161
369
  _lateProviders_1[_object_1] = true
370
+ self:deferLateProvider(object)
371
+ end
372
+ function LifecycleProvider:deferLateProvider(object)
373
+ local turn = self:findRunningTurn()
374
+ if turn ~= nil then
375
+ local _exp = self.lateTurns[turn]
376
+ local _object = object
377
+ table.insert(_exp, _object)
378
+ return nil
379
+ end
380
+ local _lateQueue = self.lateQueue
381
+ local _object = object
382
+ table.insert(_lateQueue, _object)
383
+ if self.hasLateTurn then
384
+ return nil
385
+ end
386
+ self.hasLateTurn = true
162
387
  task.defer(function()
163
- local _lateProviders_2 = self.lateProviders
164
- local _object_2 = object
165
- if not (_lateProviders_2[_object_2] ~= nil) then
166
- return nil
388
+ return self:runLateProviders()
389
+ end)
390
+ end
391
+ function LifecycleProvider:findRunningTurn()
392
+ local running = coroutine.running()
393
+ local waitingOnPromise
394
+ for turn in self.lateTurns do
395
+ local waitedOn = threadWaits[turn]
396
+ if waitedOn == running or coroutine.status(turn) == "normal" then
397
+ return turn
167
398
  end
168
- local _lateProviders_3 = self.lateProviders
169
- local _object_3 = object
170
- _lateProviders_3[_object_3] = nil
171
- local _initMembers = self.initMembers
172
- local _object_4 = object
173
- if _initMembers[_object_4] ~= nil then
174
- self:runInit(object)
399
+ if waitedOn ~= nil and not (type(waitedOn) == "thread") then
400
+ if waitingOnPromise == nil then
401
+ waitingOnPromise = turn
402
+ end
175
403
  end
176
- local _onStart = self.onStart
177
- local _object_5 = object
178
- if _onStart[_object_5] ~= nil then
404
+ end
405
+ if waitingOnPromise ~= nil and runsPromiseWork(running) then
406
+ return waitingOnPromise
407
+ end
408
+ return nil
409
+ end
410
+ function LifecycleProvider:initWaitsForRunningThread(object)
411
+ local running = coroutine.running()
412
+ for turn, providers in self.lateTurns do
413
+ if threadWaits[turn] == running or coroutine.status(turn) == "normal" then
414
+ local _condition = self.lateTurnInits[turn] == object
415
+ if not _condition then
416
+ local _object = object
417
+ _condition = table.find(providers, _object) ~= nil
418
+ end
419
+ return _condition
420
+ end
421
+ end
422
+ return false
423
+ end
424
+ function LifecycleProvider:mayWaitForRunningThread(object)
425
+ for turn, providers in self.lateTurns do
426
+ local _condition = self.lateTurnInits[turn] ~= object
427
+ if _condition then
428
+ local _object = object
429
+ _condition = not (table.find(providers, _object) ~= nil)
430
+ end
431
+ if _condition then
432
+ continue
433
+ end
434
+ local waitedOn = threadWaits[turn]
435
+ return waitedOn ~= nil and not (type(waitedOn) == "thread") and runsPromiseWork(coroutine.running())
436
+ end
437
+ return false
438
+ end
439
+ function LifecycleProvider:awaitDependencies(object)
440
+ local _initDependencies = self.initDependencies
441
+ local _object = object
442
+ local dependencies = _initDependencies[_object]
443
+ if dependencies == nil then
444
+ return not self:hasBegunExtinguishing()
445
+ end
446
+ local _initDependencies_1 = self.initDependencies
447
+ local _object_1 = object
448
+ _initDependencies_1[_object_1] = nil
449
+ local waitedFor
450
+ local since
451
+ local warned = false
452
+ while not self:hasBegunExtinguishing() do
453
+ local pending = false
454
+ for _, dependency in dependencies do
455
+ local owner = pendingInits[dependency]
456
+ if owner == nil or owner:initWaitsForRunningThread(dependency) then
457
+ continue
458
+ end
459
+ pending = true
460
+ if waitedFor == nil then
461
+ waitedFor = {}
462
+ end
463
+ if not (table.find(waitedFor, owner) ~= nil) then
464
+ table.insert(waitedFor, owner)
465
+ end
466
+ -- Once, and only for a wait that has lasted and may be on itself.
467
+ if not warned and since ~= nil and os.clock() - since >= SELF_WAIT_WARNING and owner:mayWaitForRunningThread(dependency) then
468
+ warned = true
469
+ warn(`[Flamework] '{self:getIdentifier(object)}' has waited {SELF_WAIT_WARNING}s for the onInit of '{owner:getIdentifier(dependency)}', which waits on Promise work that may be this very ignition: an onInit that ignites a module taking it after it has yielded waits for itself. Ignite such a module from onStart or a PlayerAdded handler instead.`)
470
+ end
471
+ end
472
+ if not pending then
473
+ break
474
+ end
475
+ if since == nil then
476
+ since = os.clock()
477
+ end
478
+ task.wait()
479
+ end
480
+ if self:hasBegunExtinguishing() then
481
+ return false
482
+ end
483
+ local _condition = waitedFor == nil
484
+ if not _condition then
485
+ -- ▼ ReadonlyArray.some ▼
486
+ local _result = false
487
+ local _callback = function(owner)
488
+ return owner:hasBegunExtinguishing()
489
+ end
490
+ for _k, _v in waitedFor do
491
+ if _callback(_v, _k - 1, waitedFor) then
492
+ _result = true
493
+ break
494
+ end
495
+ end
496
+ -- ▲ ReadonlyArray.some ▲
497
+ _condition = not _result
498
+ end
499
+ return _condition
500
+ end
501
+ function LifecycleProvider:runLateProviders()
502
+ -- This turn takes what was resolved before it began; anything resolved from here on, but by
503
+ -- its own `onInit`s, is the next one's.
504
+ local providers = self.lateQueue
505
+ self.lateQueue = {}
506
+ self.hasLateTurn = false
507
+ local turn = coroutine.running()
508
+ self.lateTurns[turn] = providers
509
+ local initialised = {}
510
+ -- Walked live, as `postIgnite` walks: an `onInit` may resolve another lazy provider, which
511
+ -- joins the end of this turn and is initialised after the ones before it.
512
+ while not (#providers == 0) do
513
+ -- Nothing once the module has begun to extinguish: by then it may have been told
514
+ -- `onExtinguished` -- one an `onExtinguished` handler resolved for the first time is --
515
+ -- and a later step that yields let this run after it, and before `release` detached it.
516
+ -- Nor once an `onInit` that yielded saw it begin.
517
+ if self:hasBegunExtinguishing() then
518
+ table.clear(providers)
519
+ break
520
+ end
521
+ -- Nor while the module is still igniting -- one a postIgnite hook after this plugin's
522
+ -- resolved, whose turn came when a hook yielded: it was started before the module was
523
+ -- ignited, even by an ignition that then failed. `start` gives them their turn again.
524
+ local _result = self.module
525
+ if _result ~= nil then
526
+ _result = _result.isIgnited()
527
+ end
528
+ if _result ~= true then
529
+ for _, object in providers do
530
+ local _exp = self.heldLateProviders
531
+ table.insert(_exp, object)
532
+ end
533
+ table.clear(providers)
534
+ break
535
+ end
536
+ local object = table.remove(providers, 1)
537
+ if not (self.lateProviders[object] ~= nil) then
538
+ continue
539
+ end
540
+ -- In front of its `onInit`, or of its `onStart` and per-frame events when it has none.
541
+ if not self:awaitDependencies(object) then
542
+ table.clear(providers)
543
+ break
544
+ end
545
+ if self.initMembers[object] ~= nil then
546
+ -- One that raises is reported and left out -- never ticking, never started, as on a
547
+ -- thread of its own -- and does not hold back the ones after it.
548
+ self.lateTurnInits[turn] = object
549
+ local success, err = pcall(function()
550
+ return self:runInit(object)
551
+ end)
552
+ self.lateTurnInits[turn] = nil
553
+ pendingInits[object] = nil
554
+ if not success then
555
+ task.spawn(error, err, 0)
556
+ continue
557
+ end
558
+ end
559
+ self.lateProviders[object] = nil
560
+ table.insert(initialised, object)
561
+ end
562
+ -- Over from here: one an `onStart` resolves gets a turn of its own.
563
+ self.lateTurns[turn] = nil
564
+ for _, object in initialised do
565
+ if self:hasBegunExtinguishing() then
566
+ return nil
567
+ end
568
+ if self.onStart[object] ~= nil then
179
569
  self:runStart(object)
180
570
  end
181
- end)
571
+ end
572
+ end
573
+ function LifecycleProvider:recordDependencies(object, context)
574
+ if context.dependencies == nil or #context.dependencies == 0 then
575
+ return false
576
+ end
577
+ local _initDependencies = self.initDependencies
578
+ local _object = object
579
+ local _dependencies = context.dependencies
580
+ _initDependencies[_object] = _dependencies
581
+ return true
182
582
  end
183
583
  function LifecycleProvider:addInit(object, context)
184
584
  local _initMembers = self.initMembers
@@ -191,11 +591,15 @@ do
191
591
  if context.kind ~= "provider" then
192
592
  return nil
193
593
  end
594
+ self:recordDependencies(object, context)
194
595
  if not self.hasStarted then
195
596
  local _onInit = self.onInit
196
597
  local _object_1 = object
197
598
  table.insert(_onInit, _object_1)
198
599
  else
600
+ local _object_1 = object
601
+ local _self = self
602
+ pendingInits[_object_1] = _self
199
603
  self:scheduleLateProvider(object)
200
604
  end
201
605
  end
@@ -206,9 +610,14 @@ do
206
610
  local _lateProviders = self.lateProviders
207
611
  local _object_1 = object
208
612
  _lateProviders[_object_1] = nil
209
- local _onInit = self.onInit
210
613
  local _object_2 = object
211
- local index = (table.find(_onInit, _object_2) or 0) - 1
614
+ pendingInits[_object_2] = nil
615
+ local _initDependencies = self.initDependencies
616
+ local _object_3 = object
617
+ _initDependencies[_object_3] = nil
618
+ local _onInit = self.onInit
619
+ local _object_4 = object
620
+ local index = (table.find(_onInit, _object_4) or 0) - 1
212
621
  if index ~= -1 then
213
622
  table.remove(self.onInit, index + 1)
214
623
  end
@@ -224,10 +633,17 @@ do
224
633
  if context.kind ~= "provider" then
225
634
  return nil
226
635
  end
636
+ self:recordDependencies(object, context)
227
637
  if not self.hasStarted then
228
638
  local _startOrder = self.startOrder
229
639
  local _object_1 = object
230
640
  table.insert(_startOrder, _object_1)
641
+ if context.loadOrder ~= nil and context.loadOrder ~= DEFAULT_LOAD_ORDER then
642
+ local _startLoadOrders = self.startLoadOrders
643
+ local _object_2 = object
644
+ local _loadOrder = context.loadOrder
645
+ _startLoadOrders[_object_2] = _loadOrder
646
+ end
231
647
  else
232
648
  self:scheduleLateProvider(object)
233
649
  end
@@ -239,65 +655,92 @@ do
239
655
  local _lateProviders = self.lateProviders
240
656
  local _object_1 = object
241
657
  _lateProviders[_object_1] = nil
242
- local _startOrder = self.startOrder
658
+ local _startLoadOrders = self.startLoadOrders
243
659
  local _object_2 = object
244
- local index = (table.find(_startOrder, _object_2) or 0) - 1
660
+ _startLoadOrders[_object_2] = nil
661
+ local _startOrder = self.startOrder
662
+ local _object_3 = object
663
+ local index = (table.find(_startOrder, _object_3) or 0) - 1
245
664
  if index ~= -1 then
246
665
  table.remove(self.startOrder, index + 1)
247
666
  end
248
667
  self:forget(object)
249
668
  end
250
669
  function LifecycleProvider:postIgnite(module)
670
+ self.module = module
251
671
  -- Walked live rather than over a copy: an `onInit` may resolve a lazy provider, which joins
252
672
  -- the end of the list while we iterate and is initialised in its turn -- over a copy it was
253
673
  -- skipped, and then started with the rest, never initialised. Nothing leaves the list
254
674
  -- during ignition, so the index stays true. A `while`, since a `for` compiles to a numeric
255
675
  -- loop that reads the length once.
676
+ --
677
+ -- Then the providers without an `onInit` whose `onStart` or per-frame events wait for what
678
+ -- their constructor took: what is left in `initDependencies` once every `onInit` has waited.
256
679
  local index = 0
257
- while index < #self.onInit do
258
- self:runInit(self.onInit[index + 1])
259
- index += 1
680
+ while true do
681
+ local object
682
+ local initialises = index < #self.onInit
683
+ if initialises then
684
+ object = self.onInit[index + 1]
685
+ else
686
+ for waiting in self.initDependencies do
687
+ object = waiting
688
+ break
689
+ end
690
+ if object == nil then
691
+ break
692
+ end
693
+ end
694
+ -- After the `onInit`s still pending of what its constructor took, as in a late turn: a
695
+ -- lazy provider of an import that nothing had resolved yet is constructed for it here and
696
+ -- initialised by the import's plugin, on a turn of its own, which came after this one's
697
+ -- `onInit` and `onStart` both. The ignition waits, as for an `onInit` that yields, and
698
+ -- fails if that module extinguishes meanwhile, as it does when an import extinguishes
699
+ -- while an `onInit` yields -- here before this `onInit` runs against what it released.
700
+ if not self:awaitDependencies(object) then
701
+ error(`module '{module.debugName}': '{self:getIdentifier(object)}' takes a provider of a module that was extinguished while this module was igniting`, 0)
702
+ end
703
+ if initialises then
704
+ self:runInit(object)
705
+ index += 1
706
+ end
260
707
  end
261
708
  self.hasStarted = true
262
- local _array = {}
263
- local _length = #_array
264
- local _array_1 = self.startOrder
265
- table.move(_array_1, 1, #_array_1, _length + 1, _array)
266
- for _, object in _array do
709
+ end
710
+ function LifecycleProvider:start(module)
711
+ -- An `onStart` may extinguish the module; nothing starts or ticks after that.
712
+ for _, object in self:inLoadOrder(self.startOrder) do
713
+ if self:hasBegunExtinguishing() then
714
+ return nil
715
+ end
267
716
  self:runStart(object)
268
717
  end
269
- local onTick = self.onTick
270
- local onPhysics = self.onPhysics
271
- local onRender = self.onRender
718
+ if self:hasBegunExtinguishing() then
719
+ return nil
720
+ end
721
+ for _, object in self.heldLateProviders do
722
+ self:deferLateProvider(object)
723
+ end
724
+ table.clear(self.heldLateProviders)
725
+ local tickListeners = self.tickListeners
726
+ local physicsListeners = self.physicsListeners
727
+ local renderListeners = self.renderListeners
272
728
  local connections = {}
273
729
  -- Heartbeat rather than PostSimulation: the same point of the frame in a running game, but
274
730
  -- Heartbeat also fires where no simulation runs (an edit-mode plugin, an Open Cloud Luau
275
731
  -- task), so onTick works there too. PreSimulation has no such alias; onPhysics stays silent.
276
732
  local _arg0 = RunService.Heartbeat:Connect(function(dt)
277
- for provider in onTick do
278
- self:profile(function()
279
- return provider:onTick(dt)
280
- end, provider)
281
- end
733
+ return self:walkFrame(tickListeners, "onTick", dt)
282
734
  end)
283
735
  table.insert(connections, _arg0)
284
736
  local _arg0_1 = RunService.PreSimulation:Connect(function(dt)
285
- local now = time()
286
- for provider in onPhysics do
287
- self:profile(function()
288
- return provider:onPhysics(dt, now)
289
- end, provider)
290
- end
737
+ return self:walkFrame(physicsListeners, "onPhysics", dt, time())
291
738
  end)
292
739
  table.insert(connections, _arg0_1)
293
740
  -- PreRender never fires on the server, so there is nothing to connect there.
294
741
  if RunService:IsClient() then
295
742
  local _arg0_2 = RunService.PreRender:Connect(function(dt)
296
- for provider in onRender do
297
- self:profile(function()
298
- return provider:onRender(dt)
299
- end, provider)
300
- end
743
+ return self:walkFrame(renderListeners, "onRender", dt)
301
744
  end)
302
745
  table.insert(connections, _arg0_2)
303
746
  end
@@ -305,6 +748,58 @@ do
305
748
  local _module = module
306
749
  _moduleConnections[_module] = connections
307
750
  end
751
+ function LifecycleProvider:inLoadOrder(objects)
752
+ local orders = self.startLoadOrders
753
+ if next(orders) == nil then
754
+ local _array = {}
755
+ local _length = #_array
756
+ table.move(objects, 1, #objects, _length + 1, _array)
757
+ return _array
758
+ end
759
+ self.startLoadOrders = {}
760
+ -- `table.sort` is not stable, so equals are ordered by their position.
761
+ local position = {}
762
+ -- ▼ ReadonlyArray.forEach ▼
763
+ local _callback = function(object, index)
764
+ local _object = object
765
+ local _index = index
766
+ position[_object] = _index
767
+ return position
768
+ end
769
+ for _k, _v in objects do
770
+ _callback(_v, _k - 1, objects)
771
+ end
772
+ -- ▲ ReadonlyArray.forEach ▲
773
+ local _array = {}
774
+ local _length = #_array
775
+ table.move(objects, 1, #objects, _length + 1, _array)
776
+ local sorted = _array
777
+ table.sort(sorted, function(a, b)
778
+ local _a = a
779
+ local _condition = orders[_a]
780
+ if _condition == nil then
781
+ _condition = DEFAULT_LOAD_ORDER
782
+ end
783
+ local orderA = _condition
784
+ local _b = b
785
+ local _condition_1 = orders[_b]
786
+ if _condition_1 == nil then
787
+ _condition_1 = DEFAULT_LOAD_ORDER
788
+ end
789
+ local orderB = _condition_1
790
+ local _result
791
+ if orderA ~= orderB then
792
+ _result = orderA < orderB
793
+ else
794
+ local _a_1 = a
795
+ local _exp = position[_a_1]
796
+ local _b_1 = b
797
+ _result = _exp < position[_b_1]
798
+ end
799
+ return _result
800
+ end)
801
+ return sorted
802
+ end
308
803
  function LifecycleProvider:extinguished(module)
309
804
  local _moduleConnections = self.moduleConnections
310
805
  local _module = module
@@ -320,6 +815,7 @@ do
320
815
  -- Over a copy, since a handler may detach objects; one detached by a handler before it --
321
816
  -- `removeClassInstance` from an `onExtinguished` -- has left the event, so it is not told.
322
817
  -- One failing handler must not leave the module stuck half-extinguished.
818
+ self.isExtinguishing = true
323
819
  local _array = {}
324
820
  local _length = #_array
325
821
  for _v in self.onExtinguished do
@@ -330,17 +826,61 @@ do
330
826
  if not (self.onExtinguished[provider] ~= nil) then
331
827
  continue
332
828
  end
333
- local success, err = pcall(function()
334
- return provider:onExtinguished()
335
- end)
336
- if not success then
337
- warn(`[Flamework] onExtinguished failed for '{self:getIdentifier(provider)}': {tostring(err)}`)
829
+ self:tellExtinguished(provider)
830
+ end
831
+ -- What the handlers attached is not in the copy: a lazy provider they resolved for the first
832
+ -- time. It is told in its turn, as it would have been released without it.
833
+ while not (next(self.untold) == nil) do
834
+ local _array_1 = {}
835
+ local _length_1 = #_array_1
836
+ for _v in self.untold do
837
+ _length_1 += 1
838
+ _array_1[_length_1] = _v
338
839
  end
840
+ local _binding = _array_1
841
+ local provider = _binding[1]
842
+ self.untold[provider] = nil
843
+ self:tellExtinguished(provider)
844
+ end
845
+ end
846
+ function LifecycleProvider:tellExtinguished(provider)
847
+ local success, err = pcall(function()
848
+ return provider:onExtinguished()
849
+ end)
850
+ if not success then
851
+ warn(`[Flamework] onExtinguished failed for '{self:getIdentifier(provider)}': {tostring(err)}`)
339
852
  end
340
853
  end
854
+ function LifecycleProvider:addExtinguished(object)
855
+ local _onExtinguished = self.onExtinguished
856
+ local _object = object
857
+ _onExtinguished[_object] = true
858
+ if self.isExtinguishing then
859
+ local _untold = self.untold
860
+ local _object_1 = object
861
+ _untold[_object_1] = true
862
+ end
863
+ end
864
+ function LifecycleProvider:removeExtinguished(object, refused)
865
+ if refused == nil then
866
+ refused = false
867
+ end
868
+ local _untold = self.untold
869
+ local _object = object
870
+ if _untold[_object] ~= nil then
871
+ local _untold_1 = self.untold
872
+ local _object_1 = object
873
+ _untold_1[_object_1] = nil
874
+ if not refused then
875
+ self:tellExtinguished(object)
876
+ end
877
+ end
878
+ self:removeFrom(self.onExtinguished, object)
879
+ end
341
880
  do
342
881
  -- (Flamework) LifecycleProvider metadata
343
882
  Reflect_1.defineMetadata(LifecycleProvider, "identifier", "$:lifecycle/lifecyclePlugin@LifecycleProvider")
883
+ Reflect_1.defineMetadata(LifecycleProvider, "flamework:module", script)
344
884
  Reflect_1.defineMetadata(LifecycleProvider, "flamework:implements", {})
345
885
  Reflect_1.defineMetadata(LifecycleProvider, "flamework:parameters", { "$:lifecycle/lifecyclePlugin@LifecyclePluginOptions" })
346
886
  Reflect_1.defineMetadata(LifecycleProvider, "flamework:dependencies", { {
@@ -349,20 +889,6 @@ do
349
889
  end
350
890
  LifecycleProvider = Provider()(LifecycleProvider) or LifecycleProvider
351
891
  end
352
- --* An observer that keeps one of the provider's plain event sets in step with the module.
353
- local function observeSet(provider, set)
354
- return {
355
- onAdded = function(value)
356
- local _set = set
357
- local _value = value
358
- _set[_value] = true
359
- return _set
360
- end,
361
- onRemoved = function(value)
362
- return provider:removeFrom(set, value)
363
- end,
364
- }
365
- end
366
892
  --[[
367
893
  *
368
894
  * Creates a lifecycle plugin with the specified options.
@@ -383,6 +909,9 @@ local function createLifecyclePlugin(options)
383
909
  target.onPostIgnite(function(module)
384
910
  return lifecycle:postIgnite(module)
385
911
  end)
912
+ target.onIgnited(function(module)
913
+ return lifecycle:start(module)
914
+ end)
386
915
  target.onExtinguished(function(module)
387
916
  return lifecycle:extinguished(module)
388
917
  end)
@@ -402,10 +931,15 @@ local function createLifecyclePlugin(options)
402
931
  return lifecycle:removeStart(value)
403
932
  end,
404
933
  }, "$:lifecycle/lifecycleInterfaces@OnStart")
405
- target.observe(observeSet(lifecycle, lifecycle.onTick), "$:lifecycle/lifecycleInterfaces@OnTick")
406
- target.observe(observeSet(lifecycle, lifecycle.onRender), "$:lifecycle/lifecycleInterfaces@OnRender")
407
- target.observe(observeSet(lifecycle, lifecycle.onPhysics), "$:lifecycle/lifecycleInterfaces@OnPhysics")
408
- target.observe(observeSet(lifecycle, lifecycle.onExtinguished), "$:lifecycle/lifecycleInterfaces@OnExtinguished")
934
+ lifecycle:observeFrameEvents(target)
935
+ target.observe({
936
+ onAdded = function(value)
937
+ return lifecycle:addExtinguished(value)
938
+ end,
939
+ onRemoved = function(value, context)
940
+ return lifecycle:removeExtinguished(value, context.refused == true)
941
+ end,
942
+ }, "$:lifecycle/lifecycleInterfaces@OnExtinguished")
409
943
  end
410
944
  return PluginDefinition.new("Lifecycle", setup, LIFECYCLE_SLOT)
411
945
  end