@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.
- package/README.md +6 -0
- package/flamework.build +3 -3
- package/out/flamework.luau +8 -0
- package/out/lifecycle/lifecyclePlugin.d.ts +162 -1
- package/out/lifecycle/lifecyclePlugin.luau +569 -95
- package/out/module/module.luau +213 -19
- package/out/plugin/pluginDefinition.d.ts +7 -0
- package/out/reflect.luau +11 -0
- package/out/utility/getClassImplements.d.ts +9 -2
- package/out/utility/getClassImplements.luau +89 -6
- package/out/utility/implementsCache.d.ts +16 -0
- package/out/utility/implementsCache.luau +35 -0
- package/out/utility/recycleThread.d.ts +12 -0
- package/out/utility/recycleThread.luau +29 -10
- package/out/utility/threadWaits.d.ts +36 -0
- package/out/utility/threadWaits.luau +81 -0
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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
|
|
91
|
-
local
|
|
92
|
-
_condition =
|
|
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:
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
145
|
-
|
|
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
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
-
|
|
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
|
|
258
|
-
|
|
259
|
-
index
|
|
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
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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
|
|
406
|
-
target.observe(
|
|
407
|
-
|
|
408
|
-
|
|
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
|