@flamework-experimental/core 2.0.0-alpha.3 → 2.0.0-alpha.4
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 +11 -6
- package/docs/README.md +63 -0
- package/docs/guide/01-getting-started.md +334 -0
- package/docs/guide/02-modules.md +254 -0
- package/docs/guide/03-providers.md +423 -0
- package/docs/guide/04-lifecycle-events.md +423 -0
- package/docs/guide/05-components.md +793 -0
- package/docs/guide/06-networking.md +614 -0
- package/docs/guide/07-macros.md +332 -0
- package/docs/guide/08-plugins.md +203 -0
- package/docs/guide/09-project-structure.md +392 -0
- package/docs/guide/10-migrating-from-v1.md +573 -0
- package/docs/guide/11-scopes.md +165 -0
- package/docs/guide/12-testing.md +342 -0
- package/flamework.build +1 -1
- package/out/index.d.ts +1 -0
- package/out/init.luau +1 -0
- package/out/module/module.luau +1 -1
- package/out/module/moduleBuilder.luau +1 -1
- package/out/utility/getClassesInPath.d.ts +27 -1
- package/out/utility/getClassesInPath.luau +101 -19
- package/out/utility/pathRoot.d.ts +20 -1
- package/out/utility/pathRoot.luau +80 -4
- package/package.json +14 -7
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
# 4. Lifecycle events
|
|
2
|
+
|
|
3
|
+
Lifecycle events are methods, such as `onStart` and `onTick`, that Flamework calls at set points:
|
|
4
|
+
on your providers, and also on components and on objects you attach by hand (both covered below).
|
|
5
|
+
They come from `LifecyclePlugin`, which every module includes from the start:
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
Flamework.createModule()
|
|
9
|
+
.registerProviders("src/server/services")
|
|
10
|
+
.ignite();
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
It is an ordinary plugin with no special access. `disableDefaultLifecycle()` on the builder leaves
|
|
14
|
+
it out, for a module that wants no per-frame work at all. Including one built with
|
|
15
|
+
`createLifecyclePlugin({ … })` replaces it, rather than adding a second.
|
|
16
|
+
|
|
17
|
+
## The events
|
|
18
|
+
|
|
19
|
+
Implement the interface, and the plugin finds your class.
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import { OnStart, OnTick, Provider } from "@flamework-experimental/core";
|
|
23
|
+
|
|
24
|
+
@Provider()
|
|
25
|
+
export class Spawner implements OnStart, OnTick {
|
|
26
|
+
public onStart() {
|
|
27
|
+
print("ignited");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
public onTick(dt: number) {
|
|
31
|
+
// every frame, after physics
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
| Interface | Method | Fires on |
|
|
37
|
+
|---|---|---|
|
|
38
|
+
| `OnInit` | `onInit()` | Once, during ignition, in dependency order and then `loadOrder`, before any `onStart`. May return a Promise. |
|
|
39
|
+
| `OnStart` | `onStart()` | Once, at the end of ignition, in `loadOrder`. |
|
|
40
|
+
| `OnTick` | `onTick(dt)` | `RunService.Heartbeat` |
|
|
41
|
+
| `OnPhysics` | `onPhysics(dt, time)` | `RunService.PreSimulation`; `time` is the elapsed game time. |
|
|
42
|
+
| `OnRender` | `onRender(dt)` | `RunService.PreRender` -- client only |
|
|
43
|
+
| `OnExtinguished` | `onExtinguished()` | `module.extinguish()` |
|
|
44
|
+
|
|
45
|
+
The three per-frame events repeat in a fixed cycle: `onPhysics`, then `onTick`, then `onRender`, then
|
|
46
|
+
`onPhysics` again. This follows Roblox's task scheduler: `PreSimulation` fires before the physics
|
|
47
|
+
simulation, `Heartbeat` after it, and, on the client, `PreRender` before the frame is rendered.
|
|
48
|
+
|
|
49
|
+
In a running game, `Heartbeat` fires at the same point of the frame as `PostSimulation`. Flamework
|
|
50
|
+
uses `Heartbeat` because it also fires where nothing is simulated, such as an edit-mode plugin or an
|
|
51
|
+
Open Cloud task, so `onTick` keeps working there. `PreSimulation` has no such alias, so `onPhysics`
|
|
52
|
+
does not fire in those environments.
|
|
53
|
+
|
|
54
|
+
There is nothing to register. Flamework checks each constructed object against the interfaces that
|
|
55
|
+
plugins have claimed. The check goes by name, not by shape: a class matches the interfaces listed in
|
|
56
|
+
its `implements` clause, which the transformer records as metadata on the class. A class that only
|
|
57
|
+
has the method, without `implements OnTick`, is not matched. The metadata is why the class must
|
|
58
|
+
carry a Flamework decorator for this to work at all. A parent class's `implements` clause counts
|
|
59
|
+
too, but only if the parent carries a Flamework decorator as well. See
|
|
60
|
+
[Plugins](08-plugins.md#observing-interfaces).
|
|
61
|
+
|
|
62
|
+
## `onInit` in detail
|
|
63
|
+
|
|
64
|
+
`onInit` is the setup step. It runs in order, and it can be awaited. It runs once per provider,
|
|
65
|
+
after every provider has been constructed, **in dependency order**, and everything after it waits
|
|
66
|
+
for it:
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
@Provider()
|
|
70
|
+
class Database implements OnInit {
|
|
71
|
+
public async onInit() {
|
|
72
|
+
await this.connect(); // the next provider's onInit waits for this
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
A rejected Promise fails ignition with `onInit failed for '<id>': <reason>`. Because `onInit` blocks,
|
|
78
|
+
use it only for setup that other providers really depend on. Anything else belongs in `onStart`.
|
|
79
|
+
|
|
80
|
+
### Across an import
|
|
81
|
+
|
|
82
|
+
Most of this section only matters when one module imports another (see
|
|
83
|
+
[Modules](02-modules.md#importing-a-module)).
|
|
84
|
+
|
|
85
|
+
A provider can take a lazy provider of a module it imports while that lazy provider is still running
|
|
86
|
+
its `onInit`. This happens in two ways:
|
|
87
|
+
|
|
88
|
+
- Nothing had resolved the lazy provider until this provider's constructor did. The import then
|
|
89
|
+
initialises it on a *turn* of its own: a separate batch in which the lifecycle plugin runs the
|
|
90
|
+
`onInit`s, then the `onStart`s, of lazy providers resolved too late to join ignition's own
|
|
91
|
+
`onInit` step.
|
|
92
|
+
- It was resolved earlier and is still loading.
|
|
93
|
+
|
|
94
|
+
The order is kept for what a constructor takes **directly**. The provider's `onInit` waits for the
|
|
95
|
+
lazy provider's `onInit` to finish, and the ignition waits with it, as it does for an `onInit` that
|
|
96
|
+
yields. A provider without an `onInit` waits the same way, before its `onStart` and per-frame events.
|
|
97
|
+
Providers that take nothing still initialising do not wait.
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
// in the game module
|
|
101
|
+
@Provider({ lazy: true })
|
|
102
|
+
class GameStore implements OnInit {
|
|
103
|
+
public async onInit() {
|
|
104
|
+
await this.load();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// in a module ignited per player, importing the game module
|
|
109
|
+
@Provider()
|
|
110
|
+
class PlayerData implements OnInit {
|
|
111
|
+
constructor(private store: GameStore) {}
|
|
112
|
+
|
|
113
|
+
public onInit() {
|
|
114
|
+
// GameStore's onInit has finished
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
The wait is not transitive: Flamework does not follow a provider in between that has no pending
|
|
120
|
+
`onInit` of its own. Say `PlayerInventory` takes `InventoryService`, which has no `onInit` and takes
|
|
121
|
+
the loading `DataStore`. Then `PlayerInventory` does not wait for `DataStore`. There are two fixes:
|
|
122
|
+
|
|
123
|
+
- Give `InventoryService` an `onInit`. An empty one will do: `InventoryService` then waits for
|
|
124
|
+
`DataStore`, and `PlayerInventory` waits for `InventoryService`.
|
|
125
|
+
- Have `PlayerInventory` take `DataStore` directly.
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
@Provider({ lazy: true })
|
|
129
|
+
class InventoryService implements OnInit {
|
|
130
|
+
constructor(private data: DataStore) {}
|
|
131
|
+
|
|
132
|
+
public onInit() {} // makes whatever takes this service wait for DataStore too
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
If the import begins to extinguish during the wait, the ignition fails without running that `onInit`
|
|
137
|
+
(`'<id>' takes a provider of a module that was extinguished while this module was igniting`).
|
|
138
|
+
|
|
139
|
+
The wait happens wherever the ignition runs, including in Promise work (a profile load's `andThen`,
|
|
140
|
+
an `async` handler).
|
|
141
|
+
|
|
142
|
+
**The one exception: an import's own `onInit` that ignites this module.** This applies when the
|
|
143
|
+
`onInit` ignites the module before it yields, or from a thread it started and has not got back
|
|
144
|
+
from. The ignition then does not wait for:
|
|
145
|
+
|
|
146
|
+
- what that `onInit` is itself initialising, since the `onInit` cannot finish before the ignition
|
|
147
|
+
does;
|
|
148
|
+
- a lazy provider of the import first resolved there, which joins the `onInit`'s turn.
|
|
149
|
+
|
|
150
|
+
After the `onInit` yields (an `async` `onInit` after an `await`, or a Promise callback), Flamework
|
|
151
|
+
cannot tell such an ignition apart from one started by unrelated Promise work, so it waits. A module
|
|
152
|
+
that takes a provider whose `onInit` ignites it, or a provider that joins that `onInit`'s turn, then
|
|
153
|
+
waits for itself. If such a wait lasts more than a few seconds, Flamework warns once, naming the
|
|
154
|
+
provider that waits and the one it waits for. The warning can also come for an ordinary wait on a
|
|
155
|
+
load that takes that long. Ignite such a module from `onStart` or a `PlayerAdded` handler instead.
|
|
156
|
+
|
|
157
|
+
## `onStart` in detail
|
|
158
|
+
|
|
159
|
+
`onStart` runs once per provider, at the end of ignition, **on its own thread**. Two consequences:
|
|
160
|
+
|
|
161
|
+
- **It may yield.** `task.wait`, `WaitForChild` and network calls are fine. They do not stop other
|
|
162
|
+
providers from starting.
|
|
163
|
+
- **Order between providers is their `loadOrder`**, and otherwise the order they were constructed in.
|
|
164
|
+
Each one starts on its own thread, so one runs up to its first yield before the next one starts,
|
|
165
|
+
and nothing waits for one that yields. If a provider needs another one *initialised*, inject it:
|
|
166
|
+
the injected provider's `onInit` always finishes first.
|
|
167
|
+
|
|
168
|
+
Constructors run during ignition, in dependency order, and must **not** yield. A yielding
|
|
169
|
+
constructor stalls ignition.
|
|
170
|
+
|
|
171
|
+
```ts
|
|
172
|
+
@Provider()
|
|
173
|
+
class Matchmaker implements OnStart {
|
|
174
|
+
// runs first, synchronously, in dependency order
|
|
175
|
+
constructor(private economy: Economy) {}
|
|
176
|
+
|
|
177
|
+
// runs last, on its own thread, in loadOrder
|
|
178
|
+
public onStart() {}
|
|
179
|
+
}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## Load order
|
|
183
|
+
|
|
184
|
+
`@Provider({ loadOrder })` orders `onInit` and `onStart` among the providers one ignition constructs.
|
|
185
|
+
Lower values go first. The default is `1`, as in v1.
|
|
186
|
+
|
|
187
|
+
- **Construction and `onInit`.** The module constructs its providers in ascending `loadOrder`, each
|
|
188
|
+
one after what its constructor takes, and `onInit` runs in that order. Dependency order wins: a
|
|
189
|
+
provider's dependencies are initialised before it, even when their `loadOrder` is higher. So a low
|
|
190
|
+
`loadOrder` pulls what the provider needs forward with it. Providers with the same `loadOrder` keep
|
|
191
|
+
their registration order.
|
|
192
|
+
- **`onStart`** runs in ascending `loadOrder` alone, whatever the dependencies. Among providers with
|
|
193
|
+
the same `loadOrder`, the order is the same as for `onInit`. Each one runs on its own thread up to
|
|
194
|
+
its first yield before the next one starts. So the synchronous setup of a lower `loadOrder` is done
|
|
195
|
+
before a higher one begins, which is what v1 did.
|
|
196
|
+
- **Per-frame events** (`onTick`, `onPhysics`, `onRender`) are not ordered. The listener set is
|
|
197
|
+
unordered, and sorting it would cost time every frame.
|
|
198
|
+
- **Lazy providers** are not part of the order. A lazy provider starts when it is first resolved,
|
|
199
|
+
and its `loadOrder` is ignored.
|
|
200
|
+
- **One module at a time.** An imported module ignites, and starts, before the module that imports
|
|
201
|
+
it, whatever their `loadOrder`s.
|
|
202
|
+
|
|
203
|
+
```ts
|
|
204
|
+
@Provider({ loadOrder: 10 })
|
|
205
|
+
class Heavy implements OnInit, OnStart {
|
|
206
|
+
public onInit() {}
|
|
207
|
+
public onStart() {}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
@Provider({ loadOrder: 0 })
|
|
211
|
+
class Needy implements OnInit, OnStart {
|
|
212
|
+
constructor(private heavy: Heavy) {}
|
|
213
|
+
|
|
214
|
+
public onInit() {} // after Heavy's: it needs Heavy initialised
|
|
215
|
+
public onStart() {} // first
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
@Provider()
|
|
219
|
+
class Plain implements OnInit, OnStart {
|
|
220
|
+
public onInit() {}
|
|
221
|
+
public onStart() {}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// onInit: Heavy, Needy, Plain
|
|
225
|
+
// onStart: Needy, Plain, Heavy (loadOrder alone)
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
## Ad-hoc listeners
|
|
229
|
+
|
|
230
|
+
For something that is not a provider, such as a UI component or a temporary system, `module.listen`
|
|
231
|
+
attaches a listener. It returns a function that detaches the listener again.
|
|
232
|
+
|
|
233
|
+
```ts
|
|
234
|
+
// Full form: an object implementing the interface
|
|
235
|
+
const stop = module.listen<OnTick>({
|
|
236
|
+
onTick(dt) {
|
|
237
|
+
print(dt);
|
|
238
|
+
},
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
// Shorthand: a bare function, for single-method interfaces
|
|
242
|
+
const stop = module.listen<OnTick>((dt) => print(dt));
|
|
243
|
+
|
|
244
|
+
stop();
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
`listen` attaches *after* ignition, so `onStart` is **not** replayed for a listener attached with it.
|
|
248
|
+
Per-frame events start at once.
|
|
249
|
+
|
|
250
|
+
The same applies to anything built with `createClassInstance`. It is attached to the lifecycle
|
|
251
|
+
events it implements, and detached by `removeClassInstance` or when the module extinguishes. Once
|
|
252
|
+
detached, it gets no further events, `onExtinguished` included. So an instance that an earlier
|
|
253
|
+
`onExtinguished` handler removes is not told.
|
|
254
|
+
|
|
255
|
+
`onInit` and `onStart` belong to providers. The plugin never runs them for an instance, before or
|
|
256
|
+
after ignition. Whoever created the instance is in charge of initialising and starting it.
|
|
257
|
+
|
|
258
|
+
## Lazy providers
|
|
259
|
+
|
|
260
|
+
A [lazy provider](03-providers.md#lazy-providers) is different from an instance: it is a provider.
|
|
261
|
+
So when it is first resolved after ignition, the plugin runs its `onInit` and then its `onStart`, at
|
|
262
|
+
the next resume point.
|
|
263
|
+
|
|
264
|
+
The plugin runs lazy providers in *turns*. A turn works the way ignition does for eager providers:
|
|
265
|
+
it runs every `onInit` in the order the providers were resolved, each one finished before the next
|
|
266
|
+
begins, and then every `onStart`. The cases below say which turn a lazy provider joins, what it
|
|
267
|
+
waits for, and what happens when something goes wrong:
|
|
268
|
+
|
|
269
|
+
- **Resolved together.** A lazy provider and the lazy providers its constructor takes share a turn,
|
|
270
|
+
a dependency first.
|
|
271
|
+
- **Resolved by one of the turn's `onInit`s.** It joins that turn, whether the `onInit` is sync or
|
|
272
|
+
`async`, and before or after the `onInit` yields. That covers a lazy provider resolved:
|
|
273
|
+
- on the `onInit`'s own thread;
|
|
274
|
+
- on a thread the `onInit` started and has not yet got back from;
|
|
275
|
+
- while a Promise the `onInit` returned is pending, on a thread running Promise work (an `async`
|
|
276
|
+
body, a Promise executor, an `andThen` callback). This counts any Promise's work, since
|
|
277
|
+
Flamework cannot tell which Promise a thread works for.
|
|
278
|
+
- **Resolved anywhere else meanwhile.** It gets its own turn. A thread that an `onInit` spawned
|
|
279
|
+
counts as "anywhere else" once it has yielded. Its `onInit` (or, without one, its `onStart` and
|
|
280
|
+
per-frame events) waits only for the `onInit`s still running of the providers its constructor
|
|
281
|
+
takes directly. So if another turn is still initialising one of those dependencies, this provider
|
|
282
|
+
is initialised once that dependency's `onInit` has finished, and never sees it half-initialised.
|
|
283
|
+
The wait is not transitive; see [Across an import](#across-an-import).
|
|
284
|
+
- **Waiting in a circle.** A dependency that is itself waiting for what depends on it hangs both, as
|
|
285
|
+
with eager providers.
|
|
286
|
+
- **An `onInit` that raises.** The error is reported, and that provider never ticks and is never
|
|
287
|
+
started. The providers after it, or waiting for it, carry on.
|
|
288
|
+
- **Resolved while the module is still igniting**, by a plugin's `onPostIgnite` hook that runs after
|
|
289
|
+
the lifecycle plugin's. It waits for ignition to finish, and gets neither `onInit` nor `onStart` if
|
|
290
|
+
the ignition fails. Its per-frame events wait for that too: it does not tick before its `onInit`
|
|
291
|
+
has finished.
|
|
292
|
+
- **Once the module has begun to extinguish**, neither `onInit` nor `onStart` runs for a lazy
|
|
293
|
+
provider that has not had them yet, whenever it was resolved. One first resolved by an
|
|
294
|
+
`onExtinguished` handler is told `onExtinguished`, and that is all it hears.
|
|
295
|
+
|
|
296
|
+
## Components
|
|
297
|
+
|
|
298
|
+
Components are constructed through the module that includes `ComponentPlugin`. So they get their
|
|
299
|
+
per-frame events from **that module's** lifecycle plugin: the default one, unless the module
|
|
300
|
+
disabled it, in which case components do not tick. `onInit` and `onStart` are the exceptions:
|
|
301
|
+
`Components` calls both itself, so they work either way.
|
|
302
|
+
|
|
303
|
+
- `onInit` runs synchronously, right after construction, before the component can be seen anywhere:
|
|
304
|
+
before `getComponent` hands it back, before another component receives it through a link, and
|
|
305
|
+
before an added listener hears of it. A Promise it returns is not awaited. If it raises, the
|
|
306
|
+
component stays in place but is invalid, hidden from everything until the tracker rebuilds it.
|
|
307
|
+
- `onStart` runs on its own thread once the component is attached, and not before ignition has
|
|
308
|
+
finished. So a component built from a provider's `onInit` starts once every provider has started.
|
|
309
|
+
|
|
310
|
+
**Tagged instances get their components after the providers start.** The component plugin starts
|
|
311
|
+
watching tags once the module has ignited, in its `onIgnited` hook, which runs after the lifecycle
|
|
312
|
+
plugin has started the providers: every provider's `onStart` has been called, whatever its
|
|
313
|
+
`loadOrder`, and has run up to its first yield. So in a provider's `onStart`:
|
|
314
|
+
|
|
315
|
+
- `getAllComponents<T>()` and `getComponents<T>(instance)` find none of the instances tagged before
|
|
316
|
+
ignition (`getComponent` still builds one on demand);
|
|
317
|
+
- `onComponentAdded<T>(cb)` hears about each of them as it is built. It never replays components
|
|
318
|
+
that already exist, so connect it there, and read `getAllComponents<T>()` first only when you
|
|
319
|
+
subscribe later: after a yield, from a lazy provider, or from an event handler.
|
|
320
|
+
|
|
321
|
+
See [Components](05-components.md#lifecycle).
|
|
322
|
+
|
|
323
|
+
## Profiling
|
|
324
|
+
|
|
325
|
+
In Studio, every per-frame callback runs under `debug.profilebegin` and `debug.setmemorycategory`,
|
|
326
|
+
labelled with the provider's id. So providers show up by name in the MicroProfiler and the memory
|
|
327
|
+
view. To force profiling on or off, build the plugin with options instead of using the default:
|
|
328
|
+
|
|
329
|
+
```ts
|
|
330
|
+
import { createLifecyclePlugin } from "@flamework-experimental/core";
|
|
331
|
+
|
|
332
|
+
Flamework.createModule()
|
|
333
|
+
.includePlugin(createLifecyclePlugin({ profiling: false }))
|
|
334
|
+
.ignite();
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
Including a configured plugin replaces the default, so the module still runs exactly one. The
|
|
338
|
+
project-wide default is `core.profiling` in `flamework.config.json`. This option overrides it for one
|
|
339
|
+
module.
|
|
340
|
+
|
|
341
|
+
The id each object is profiled under is looked up once, and remembered until the object leaves its
|
|
342
|
+
last lifecycle event. So components that come and go leave nothing behind.
|
|
343
|
+
|
|
344
|
+
## Asking what is attached
|
|
345
|
+
|
|
346
|
+
The plugin provides its `LifecycleProvider`, so you can ask a module what it is currently running:
|
|
347
|
+
|
|
348
|
+
```ts
|
|
349
|
+
import { LifecycleProvider } from "@flamework-experimental/core";
|
|
350
|
+
|
|
351
|
+
const lifecycle = module.resolveDependency<LifecycleProvider>();
|
|
352
|
+
print(lifecycle.onTick.size(), "objects are ticking");
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
`onStart`, `onTick`, `onPhysics`, `onRender` and `onExtinguished` are the live sets, one per module.
|
|
356
|
+
They are there to be read. The plugin fills and empties them from the interfaces a class implements.
|
|
357
|
+
To attach something by hand, use `listen`.
|
|
358
|
+
|
|
359
|
+
```ts
|
|
360
|
+
@Injectable()
|
|
361
|
+
class Countdown implements OnTick {
|
|
362
|
+
public onTick(dt: number) {}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const countdown = module.createClassInstance(Countdown); // starts ticking
|
|
366
|
+
module.removeClassInstance(countdown); // stops
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
## Patterns
|
|
370
|
+
|
|
371
|
+
**Constructor for wiring, `onStart` for work.** Take your dependencies in the constructor and do
|
|
372
|
+
nothing else there. Put anything that yields, waits on replication or touches the world in
|
|
373
|
+
`onStart`.
|
|
374
|
+
|
|
375
|
+
**Keep `OnRender` on the client.** `PreRender` does not fire on the server, so a provider
|
|
376
|
+
implementing `OnRender` does nothing there. Still, it is clearer to register it only in the client
|
|
377
|
+
module.
|
|
378
|
+
|
|
379
|
+
**Clean up in `OnExtinguished`.** If a provider opens connections that outlive it (signals, threads,
|
|
380
|
+
Instances), close them there. Then a module that extinguishes leaves nothing behind.
|
|
381
|
+
|
|
382
|
+
```ts
|
|
383
|
+
@Provider()
|
|
384
|
+
class Broadcaster implements OnExtinguished {
|
|
385
|
+
private connection = someSignal.Connect(() => {});
|
|
386
|
+
|
|
387
|
+
public onExtinguished() {
|
|
388
|
+
this.connection.Disconnect();
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
**One listener, many objects.** If you have hundreds of short-lived objects that need a tick, use one
|
|
394
|
+
provider that loops over them rather than hundreds of `listen` calls.
|
|
395
|
+
|
|
396
|
+
## Caveats
|
|
397
|
+
|
|
398
|
+
- **`disableDefaultLifecycle()` is silent.** Nothing warns you that `onStart` never ran.
|
|
399
|
+
- **One lifecycle plugin per module.** Including a configured one on the builder replaces the
|
|
400
|
+
default. A plugin that includes a second one is refused at ignition, so include it on the module
|
|
401
|
+
instead.
|
|
402
|
+
- **Per-frame events are unordered.** `loadOrder` orders `onInit` and `onStart` only; the per-frame
|
|
403
|
+
listener set is unordered.
|
|
404
|
+
- **`listen` does not replay `onStart`.** It attaches from that moment on.
|
|
405
|
+
- **Extinguishing disconnects everything.** The plugin disconnects its `RunService` connections and
|
|
406
|
+
releases the providers, so a module that has been extinguished stops ticking. (This was once a
|
|
407
|
+
bug; a spec covers it now.) Nothing ticks or starts from the moment `extinguish()` is called. That
|
|
408
|
+
includes the time while the modules importing it go down first, whose `onExtinguished` handlers
|
|
409
|
+
may yield.
|
|
410
|
+
- **A failing `onExtinguished` does not abort extinguish.** Flamework warns about it, and the
|
|
411
|
+
remaining handlers still run, so the module cannot get stuck half-extinguished. The same goes for
|
|
412
|
+
a plugin's extinguished hook.
|
|
413
|
+
- **A failing ignition is extinguished.** An error raised during ignition (in a constructor, an
|
|
414
|
+
`onInit` or a plugin's hook) first runs the extinguished hooks for what had been set up, so nothing
|
|
415
|
+
keeps ticking. Then the error comes out of `ignite()`.
|
|
416
|
+
- **`onInit` blocks.** A yielding `onInit` delays every provider after it. A rejected Promise fails
|
|
417
|
+
ignition.
|
|
418
|
+
- **A yielding constructor stalls ignition**, because construction is synchronous. Yield in
|
|
419
|
+
`onStart`.
|
|
420
|
+
|
|
421
|
+
---
|
|
422
|
+
|
|
423
|
+
Previous: [Providers](03-providers.md) · Next: [Components](05-components.md)
|