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