@flamework-experimental/core 2.0.0-alpha.2 → 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.
Files changed (44) hide show
  1. package/README.md +41 -34
  2. package/docs/README.md +63 -0
  3. package/docs/guide/01-getting-started.md +334 -0
  4. package/docs/guide/02-modules.md +254 -0
  5. package/docs/guide/03-providers.md +423 -0
  6. package/docs/guide/04-lifecycle-events.md +423 -0
  7. package/docs/guide/05-components.md +793 -0
  8. package/docs/guide/06-networking.md +614 -0
  9. package/docs/guide/07-macros.md +332 -0
  10. package/docs/guide/08-plugins.md +203 -0
  11. package/docs/guide/09-project-structure.md +392 -0
  12. package/docs/guide/10-migrating-from-v1.md +573 -0
  13. package/docs/guide/11-scopes.md +165 -0
  14. package/docs/guide/12-testing.md +342 -0
  15. package/flamework.build +1 -1
  16. package/out/dependency.d.ts +4 -0
  17. package/out/dependency.luau +4 -0
  18. package/out/index.d.ts +5 -2
  19. package/out/init.luau +11 -2
  20. package/out/lifecycle/lifecyclePlugin.d.ts +13 -1
  21. package/out/lifecycle/lifecyclePlugin.luau +67 -7
  22. package/out/module/module.luau +126 -24
  23. package/out/module/moduleBuilder.d.ts +12 -3
  24. package/out/module/moduleBuilder.luau +23 -1
  25. package/out/module/moduleDefinition.d.ts +6 -0
  26. package/out/module/providerRegistration.d.ts +8 -0
  27. package/out/module/providerRegistration.luau +29 -0
  28. package/out/plugin/pluginDefinition.d.ts +7 -4
  29. package/out/provider.d.ts +19 -0
  30. package/out/provider.luau +8 -0
  31. package/out/reflect.luau +6 -0
  32. package/out/utility/explainUnresolved.d.ts +9 -0
  33. package/out/utility/explainUnresolved.luau +43 -0
  34. package/out/utility/getClassesInPath.d.ts +37 -3
  35. package/out/utility/getClassesInPath.luau +154 -33
  36. package/out/utility/globs.d.ts +2 -2
  37. package/out/utility/globs.luau +3 -3
  38. package/out/utility/leftOut.d.ts +35 -0
  39. package/out/utility/leftOut.luau +171 -0
  40. package/out/utility/moduleClasses.d.ts +9 -0
  41. package/out/utility/moduleClasses.luau +64 -0
  42. package/out/utility/pathRoot.d.ts +20 -1
  43. package/out/utility/pathRoot.luau +80 -4
  44. package/package.json +14 -7
@@ -0,0 +1,793 @@
1
+ # 5. Components
2
+
3
+ A **component** is a class attached to an Instance, usually through a CollectionService tag.
4
+ Flamework constructs one for each tagged instance, checks its attributes and its instance tree, and
5
+ destroys it when the tag is removed.
6
+
7
+ ```sh
8
+ npm install @flamework-experimental/components
9
+ ```
10
+
11
+ Map it in your Rojo project next to `core` ([Getting started › Rojo](01-getting-started.md#rojo)).
12
+
13
+ ## Your first component
14
+
15
+ ```ts
16
+ // src/shared/components/vehicle.ts
17
+ import { BaseComponent, Component } from "@flamework-experimental/components";
18
+ import { OnStart } from "@flamework-experimental/core";
19
+
20
+ interface Attributes {
21
+ speed: number;
22
+ label?: string;
23
+ }
24
+
25
+ @Component({ tag: "Vehicle" })
26
+ export class Vehicle extends BaseComponent<Attributes, Model> implements OnStart {
27
+ public onStart() {
28
+ print(this.instance.Name, this.attributes.speed);
29
+ }
30
+ }
31
+ ```
32
+
33
+ `BaseComponent<A, I>` gives you `this.instance`, typed as `I`, and `this.attributes`, typed as
34
+ `Readonly<A>`. The transformer reads both type parameters and generates *guards* from them: checks
35
+ that run at runtime.
36
+
37
+ Register the components and include the plugin:
38
+
39
+ ```ts
40
+ import { ComponentPlugin } from "@flamework-experimental/components";
41
+
42
+ Flamework.createModule()
43
+ .includePlugin(ComponentPlugin.fromPath("src/shared/components"))
44
+ .registerProviders("src/server/services")
45
+ .ignite();
46
+ ```
47
+
48
+ Tag a `Model` with `Vehicle` in Studio, set a `speed` attribute, and the component is constructed.
49
+
50
+ ### Shorthand vs full form
51
+
52
+ ```ts
53
+ // Shorthand: register a folder and build the plugin in one call
54
+ ComponentPlugin.fromPath("src/shared/components");
55
+
56
+ // Full form: the same thing, with room to add more
57
+ ComponentPlugin.createPlugin()
58
+ .registerComponents("src/shared/components")
59
+ .registerComponent(SpecialCase)
60
+ .build();
61
+ ```
62
+
63
+ ### Lifecycle
64
+
65
+ Components are constructed through the module that includes `ComponentPlugin`. So they get
66
+ `onTick`, `onPhysics` and `onRender` from **that module's** lifecycle plugin, which every module
67
+ starts with. `onInit` and `onStart` are the exceptions: `Components` calls both itself, so they work
68
+ even with `disableDefaultLifecycle()`. In order:
69
+
70
+ | Step | When |
71
+ |---|---|
72
+ | constructor | Dependencies are injected. `this.instance`, `this.attributes` and every link are already resolved. |
73
+ | `onInit()` | Runs synchronously, right after construction, **before the component can be seen**: `getComponent` has not handed it back yet, no other component holds it in `childComponents` or `attributeComponents`, and no `onComponentAdded` listener has heard of it. A Promise it returns is not awaited. If it raises, the component is **invalid** (see below). |
74
+ | attached | `getComponent` answers, links resolve to it, added listeners fire. |
75
+ | `onStart()` | Runs on its own thread, after the component is attached, and not before ignition has finished. So a component built from a provider's `onInit` starts once every provider has started. A component that removes itself here is announced as added and then as removed, and no `waitForComponent` is resolved with it. |
76
+ | per-frame events | From the module's lifecycle plugin. |
77
+ | `destroy()` | When the component is removed. `BaseComponent`'s own releases only the `onAttributeChanged` handlers; see [Cleaning up](#cleaning-up). |
78
+
79
+ Put the setup that anything else may rely on in `onInit`. Another component that links to this one
80
+ (see [Links](#links)) receives it already initialised, whichever of the two was tagged first.
81
+
82
+ **Tagged instances get their components once the module has ignited**, after every provider's
83
+ `onStart` has been called and has run up to its first yield: the plugin starts watching tags in its
84
+ `onIgnited` hook, after the lifecycle plugin has started the providers. So a provider's `onStart`
85
+ finds none of them with `getAllComponents<T>()` or `getComponents<T>(instance)`, and
86
+ `onComponentAdded<T>(cb)` connected there hears about each one as it is built. That listener never
87
+ replays components that already exist, so a listener connected later (after a yield, from a lazy
88
+ provider, from an event handler) reads `getAllComponents<T>()` first. `getComponent` is the
89
+ exception: it builds a qualifying component on demand, at any time. See
90
+ [Lifecycle events › Components](04-lifecycle-events.md#components).
91
+
92
+ ### Cleaning up
93
+
94
+ `BaseComponent.destroy()` releases only what Flamework connected for the component, the handlers
95
+ behind `onAttributeChanged`. Removing the tag, or `removeComponent`, leaves the instance where it
96
+ is, so a connection of your own keeps firing into a component that has gone. Disconnect it in
97
+ `destroy`, and call `super.destroy()`:
98
+
99
+ ```ts
100
+ @Component({ tag: "Coin" })
101
+ export class Coin extends BaseComponent<{}, BasePart> implements OnStart {
102
+ private touched?: RBXScriptConnection;
103
+
104
+ public onStart() {
105
+ this.touched = this.instance.Touched.Connect((part) => this.collect(part));
106
+ }
107
+
108
+ public override destroy() {
109
+ this.touched?.Disconnect();
110
+ super.destroy();
111
+ }
112
+
113
+ private collect(part: BasePart) {}
114
+ }
115
+ ```
116
+
117
+ `destroy` also runs for every component when the module extinguishes. A `destroy` that raises does
118
+ not hold up the teardown (see [Caveats](#caveats)).
119
+
120
+ **Removing the component from its own constructor or `onInit`** undoes the construction as it
121
+ finishes. This covers `removeComponent`, and taking its tag away where the place delivers signals
122
+ immediately. The component is destroyed, and never attached, started or announced.
123
+
124
+ **An `onInit` that raises** does not take the component away, and does not build another one. The
125
+ component stays where it is, marked invalid:
126
+
127
+ - it gets no `onStart` and no per-frame events;
128
+ - it is absent from `getComponent`, `getComponents`, `getAllComponents` and `waitForComponent`;
129
+ - no added listener hears of it;
130
+ - a component that links to it keeps waiting. The warning says
131
+ `carries an invalid '...', whose onInit raised: ...`.
132
+
133
+ It still holds its place. Nothing is built on top of it until the *tracker* takes it down for a
134
+ reason of its own: the tag goes, the tree breaks, a link is lost. (The tracker is the part of
135
+ Flamework that watches instances and builds and removes their components.) The component built once
136
+ that reason has passed is a fresh one, with its own `onInit`.
137
+
138
+ The failure is reported in two ways. For a tagged instance, Flamework warns
139
+ (`Failed to instantiate ...`). `addComponent` by hand raises
140
+ `component '...' failed to initialise for ...`, and raises again, with `waiting to be removed`,
141
+ while the invalid component is there. `removeComponent` clears it.
142
+
143
+ Register by glob when the components are spread across feature folders:
144
+
145
+ ```ts
146
+ ComponentPlugin.fromGlob("src/**/components");
147
+ ```
148
+
149
+ A module may include several component plugins, in any mix: a `fromPath` per folder, a `fromGlob`,
150
+ one built by hand. They share **one** `Components` for that module:
151
+
152
+ - every class any of them registers ends up in it;
153
+ - a component in one can link to a component in another;
154
+ - `Dependency<Components>()` and constructor injection get that one `Components`.
155
+
156
+ A class that two plugins register is registered once, and kept when either registration's scope
157
+ holds. A module that imports another keeps its own `Components` if it includes a component plugin of
158
+ its own. If it includes none, it resolves the import's `Components`.
159
+
160
+ ```ts
161
+ Flamework.createModule()
162
+ .includePlugin(ComponentPlugin.fromPath("src/shared/components"))
163
+ .includePlugin(ComponentPlugin.fromPath("src/server/components"))
164
+ .ignite();
165
+ ```
166
+
167
+ Path and glob registration find every component defined in the files there, exported or not, the
168
+ way `registerProviders` finds providers ([Providers](03-providers.md#how-it-actually-works)).
169
+
170
+ As with providers, only classes decorated with `@Component()` **themselves** are registered. An
171
+ exported but undecorated subclass is skipped, and `registerComponent` raises an error for one.
172
+
173
+ ## Attributes
174
+
175
+ Attribute guards are generated from the first type parameter. An instance whose attributes do not
176
+ match is rejected: the component is not created, and `addComponent` throws
177
+ `... has invalid attribute 'speed' for '...'`.
178
+
179
+ Optional properties really are optional: `label?: string` accepts a missing attribute.
180
+
181
+ ### Defaults
182
+
183
+ Instead of rejecting the instance, you can write a default value back to it:
184
+
185
+ ```ts
186
+ @Component({ tag: "Vehicle", defaults: { speed: 16 } })
187
+ ```
188
+
189
+ A missing or invalid `speed` becomes `16`, and the attribute is set on the instance.
190
+
191
+ ### Reacting to changes
192
+
193
+ Attributes are tracked by default, so `this.attributes` stays current:
194
+
195
+ ```ts
196
+ this.onAttributeChanged("speed", (newValue, oldValue) => {
197
+ print(`${oldValue} -> ${newValue}`);
198
+ });
199
+ ```
200
+
201
+ A value that fails the guard is never applied:
202
+
203
+ - With no `defaults` entry for the attribute, the component is removed. Once the attribute is valid,
204
+ the component is built again, reading the attributes afresh.
205
+ - With a `defaults` entry, the component keeps its last good value.
206
+
207
+ Turn tracking off with `refreshAttributes: false`, which also disables `onAttributeChanged`. The
208
+ validity of the attributes is watched either way. Like the instance tree, it is a *criterion*: a
209
+ condition the component needs in order to exist.
210
+
211
+ ### Writing an attribute
212
+
213
+ Assigning to `this.attributes` writes the value back to the instance:
214
+
215
+ ```ts
216
+ this.attributes.speed = 32;
217
+ this.attributes.speed += 8;
218
+ this.attributes.speed++;
219
+ delete this.attributes.label;
220
+ ```
221
+
222
+ The write has to be spelled `<component>.attributes.<name>`, because that is the shape the
223
+ transformer rewrites. The component does not have to be `this`: a component reached through
224
+ `getComponent` is written the same way. A write through a local
225
+ (`const attributes = this.attributes; attributes.speed = 32`) is an ordinary table write, and the
226
+ instance never hears about it. A read-modify-write (`+=`, `++`, `--`) evaluates the component
227
+ expression a second time for the value it computes, so keep side effects out of that expression.
228
+
229
+ Every write is checked against the same guard the attribute was accepted with, and raises an error
230
+ if it fails. This catches a write that a cast let through:
231
+
232
+ ```ts
233
+ // Raises: 'fast' is not a valid value for attribute 'speed' of '...'
234
+ this.attributes.speed = someString as unknown as number;
235
+ ```
236
+
237
+ Without the check, the component would hold a value that its own declared type says is impossible.
238
+ The instance would carry it too, and reject the component the next time one is built. Writing
239
+ `undefined` to a required attribute raises for the same reason. An optional attribute accepts
240
+ `undefined`, and the attribute is cleared.
241
+
242
+ ### Overriding a guard
243
+
244
+ To use your own guard for an attribute instead of the generated one, pass it in `attributes`:
245
+
246
+ ```ts
247
+ @Component({
248
+ tag: "Vehicle",
249
+ attributes: { speed: t.numberPositive },
250
+ })
251
+ ```
252
+
253
+ ## Instance guards
254
+
255
+ The second type parameter is the instance tree. `BaseComponent<{}, Part>` will not attach to a
256
+ Folder. To require children, intersect it with an object type, as deep as you like:
257
+
258
+ ```ts
259
+ // Requires a Humanoid child, and a Head with a Face, before the component is created
260
+ @Component({ tag: "Character" })
261
+ export class Character extends BaseComponent<{}, Model & { Humanoid: Humanoid; Head: BasePart & { Face: Decal } }> {}
262
+ ```
263
+
264
+ The transformer writes the tree down as data: the classes each instance may be, and the children it
265
+ must have, by name. Flamework reads the tree the way your code reads `this.instance.Head`: with
266
+ `FindFirstChild`, which returns the first child of that name. So a second child with the same name
267
+ is not an error, and it is not the one that is checked. It can come and go without the component
268
+ noticing.
269
+
270
+ A child may be a union of classes (`Texture | Decal`). A union of whole trees, such as
271
+ `(Model & { Root: Part }) | (Folder & { Core: Folder })`, cannot be written down this way. It gets a
272
+ `t` guard instead, which can only be re-run whole.
273
+
274
+ A mismatch is named. `addComponent` raises with `child 'Head.Face' is missing (expected Decal)` or
275
+ `child 'Head' is a Folder, expected BasePart`, and the warning for a tagged instance that never
276
+ qualifies says the same (see [Streaming](#streaming)).
277
+
278
+ A child cannot be optional, and Flamework rejects one when you build:
279
+
280
+ ```ts
281
+ // Rejected: `this.instance.Head` would error whenever the child is missing
282
+ export class Character extends BaseComponent<{}, Model & { Head?: BasePart }> {}
283
+ ```
284
+
285
+ `this.instance.Head` indexes the instance itself, and Roblox raises an error for a child that is not
286
+ there, rather than returning nothing. Even the `if (this.instance.Head)` you would write to check
287
+ for it raises. So the optional type would promise a read that cannot be made. Either require the
288
+ child, or leave it out of the tree and get it with `FindFirstChild`. A child that names a
289
+ **component** is no exception: name a component that may or may not be there with an optional link
290
+ attribute, or look it up with `getComponent`. See [links](#links).
291
+
292
+ Attributes work differently and stay optional: a missing one reads back as `undefined`, so
293
+ `label?: string` is fine.
294
+
295
+ If the generated guard is not what you want, replace it entirely with `instanceGuard`. A guard
296
+ written by hand can only say that it failed, and it can only be re-run whole when the tree changes.
297
+
298
+ ## Links
299
+
300
+ A **link** is an attribute or a child that names another instance. Flamework waits for the named
301
+ instance, keeps the link resolved, and takes the component down again if the instance goes away. A
302
+ link can name an instance, or a component on an instance.
303
+
304
+ ### Instance attributes
305
+
306
+ An attribute typed as an Instance is stored on the instance as an `InstanceHandle`, which is what
307
+ Roblox's own instance-valued attributes are:
308
+
309
+ ```ts
310
+ interface Attributes {
311
+ Target: BasePart;
312
+ Spare?: BasePart;
313
+ }
314
+
315
+ @Component({ tag: "Turret" })
316
+ export class Turret extends BaseComponent<Attributes, Model> {
317
+ public onStart() {
318
+ // The handle is resolved for you; this is the part itself.
319
+ print(this.attributes.Target.Position);
320
+ }
321
+ }
322
+ ```
323
+
324
+ The component is not constructed until the handle resolves. A handle is empty until the instance it
325
+ names has streamed in at least once. So under StreamingEnabled, a far-away target keeps the
326
+ component waiting. Once the target has streamed in, the handle stays resolved, even if the target
327
+ streams back out.
328
+
329
+ Assigning to the attribute writes a fresh handle, after checking the instance the same way the link
330
+ was resolved:
331
+
332
+ ```ts
333
+ this.attributes.Target = otherPart;
334
+ ```
335
+
336
+ The check is the whole guard, structure included. A link to a component that declares
337
+ `Model & { Root: BasePart }` only accepts a model that has that child. Assigning an instance that
338
+ could never be right raises an error.
339
+
340
+ An attribute typed `InstanceHandle` is left alone: you get the handle, and nothing waits. Use this
341
+ when you want to do the resolving yourself.
342
+
343
+ `defaults` works here as it does elsewhere. Give an instance as the default, and an attribute that
344
+ was never written is filled in with a handle for it, instead of keeping the component waiting. That
345
+ holds for an optional link too, whose guard would accept the attribute being missing. The default is
346
+ written to the instance either way, so the component and the instance never disagree about what the
347
+ link names. The default stands in for an attribute the component was **built** without, not for one
348
+ the component has since cleared. Clearing an optional link clears it, and the default is
349
+ applied again the next time a component is built.
350
+
351
+ ### Naming a component
352
+
353
+ Type an attribute or a child as a **component** rather than an Instance, and the instance it names
354
+ has to carry that component:
355
+
356
+ ```ts
357
+ interface Tree extends Model {
358
+ EffectHandler: EffectHandlerComponent;
359
+ Barrel: BasePart;
360
+ }
361
+
362
+ @Component({ tag: "Turret" })
363
+ export class Turret extends BaseComponent<{ Owner: PlayerComponent }, Tree> {
364
+ public onStart() {
365
+ // `instance` holds instances, and the components sit beside it.
366
+ const part: BasePart = this.instance.EffectHandler;
367
+
368
+ this.childComponents.EffectHandler.playEffect();
369
+ this.attributeComponents.Owner.credit();
370
+ }
371
+ }
372
+ ```
373
+
374
+ `this.instance` still holds instances: `this.instance.EffectHandler` is the part the component is
375
+ attached to, and that is what the generated instance guard checks. The components themselves live in
376
+ `childComponents` and `attributeComponents`. Their fields are readonly: Flamework owns them, and
377
+ reassigning one would only put it out of step with the instance.
378
+
379
+ The tree *under* a child that names a component is that component's business, not the owner's. The
380
+ owner's shape stops at the child's class. Whether the rest is there is decided by the linked
381
+ component's own tracker, under its own streaming mode. (A component type cannot be intersected with
382
+ a tree of its own, since `Handler & { Root: Part }` is not a type, so there is nothing the owner
383
+ could add.)
384
+
385
+ `Turret` is not constructed until `EffectHandler` exists **and** carries its component, in either
386
+ tag order. It is removed again if that component goes away. This uses the same criteria mechanism
387
+ as component dependencies and streaming, so the warning that lists what a component is waiting for
388
+ names the link.
389
+
390
+ A link resolves the class it names and **nothing else**. A subclass does not stand in for its
391
+ parent. An instance carrying several components hands back the one the link names, not whichever
392
+ came first, so there is no ambiguity to resolve. This works in both directions: another component
393
+ leaving the linked instance changes nothing, even a subclass of the one the link names. The guard
394
+ is the whole shape too: a link to a component declaring `Model & { Root: BasePart }` only accepts a
395
+ model that has that child.
396
+
397
+ A link waits for what `getComponent` would hand back, and on top of that it respects the ancestor
398
+ lists (see [Where components may attach](#where-components-may-attach)). If a `predicate` refuses
399
+ the linked component, or its instance sits under a blocked ancestor, the link stays unmet and the
400
+ component is not built. A link never reports itself met and then fails to build the component. It
401
+ also never builds the linked component there itself: pointing a link attribute at a tagged instance
402
+ under a blocked ancestor leaves the link unmet, rather than constructing the component the ancestor
403
+ lists refused. The ancestor lists gate *construction*, not the link. So a component that is already
404
+ attached to a blocked instance (added by hand, or built by a `getComponent` of your own) does
405
+ satisfy the link.
406
+
407
+ "What `getComponent` would hand back" is the whole rule. A link is met by an instance that already
408
+ carries the component, **or** by one that Flamework would build it on. The answer is the same
409
+ whether or not anything is tracking that instance yet. So a spawner can tag a whole tree and ask for
410
+ its component straight away. Tag announcements arrive a resumption later, and `getComponent` builds
411
+ the link's component on the way to building yours, rather than refusing because the announcement
412
+ has not arrived yet.
413
+
414
+ A link that names its own component on its own instance is unmet for the same reason. It is the one
415
+ link that can never be met on the way in, because the component would already have to exist to be
416
+ built. So the component is not built, and the link reports this instead of raising an error out of
417
+ the tag that asked for it. Point the attribute somewhere else (or, if it is optional, clear it) and
418
+ the component is built. Pointing it back at its own instance afterwards resolves to the component
419
+ that is now there. A ring of links works the same way, however many instances it goes round: none of
420
+ it can be built from nothing, so the ring stays unmet until something in it exists for another
421
+ reason.
422
+
423
+ That promise covers a rebuild as well. Roblox delivers the tree's signals a resumption late, so a
424
+ change that takes a component down and a change that should keep it down can arrive one after the
425
+ other. That is why every link is read from the instance again on the way in, rather than trusted to
426
+ still be what it last reported. A component is built only when the tree agrees.
427
+
428
+ The guard is also kept current, not read once when the attribute is written. A link to a component
429
+ declaring `Model & { Root: BasePart }` is unmet while the model it names has no `Root`, and becomes
430
+ met when one is parented in. So an attribute may be written before the instance it names is
431
+ finished, and the component is built once that instance is finished.
432
+
433
+ A component can only be named as a **direct** member of the tree. One further down is a compile
434
+ error, because `this.instance` would have nowhere to put it. Declare it on the component attached to
435
+ that child instead, or look it up with `getComponent`.
436
+
437
+ A child naming a component cannot be optional, just as a plain child cannot: `this.instance.Core`
438
+ still indexes the instance, and raises while the child is missing. Name a component that may or may
439
+ not be there through an [attribute](#instance-attributes) instead, which can be optional, or look it
440
+ up with `getComponent` when you need it.
441
+
442
+ #### Writing one
443
+
444
+ Sometimes you assign an instance that is the right shape but does not carry the component **yet**.
445
+ That is a matter of timing, not a bad value, so it does not raise. But writing it would make the
446
+ component doing the writing stop qualifying, and destroy it in the middle of a method. So instead,
447
+ the write is refused and Flamework warns. Wait for the component first:
448
+
449
+ ```ts
450
+ // Components has to be injected for this; ComponentMetadata comes first.
451
+ const [ok] = this.components.waitForComponent<Rig>(target).timeout(5).await();
452
+ if (!ok) return warn("that instance never got its component");
453
+
454
+ this.attributes.Rigged = target;
455
+ ```
456
+
457
+ The resolved attribute type already asks for the linked component's instance type, so most mistakes
458
+ here are compile errors. The guard catches the ones a cast let through.
459
+
460
+ Writing an instance under a blocked ancestor is handled the same way, because a link never builds a
461
+ component where the ancestor lists keep one out. The write is refused with a warning, whether or not
462
+ that instance is tagged.
463
+
464
+ ### What takes a component down again
465
+
466
+ | Change | Effect |
467
+ |---|---|
468
+ | The tag is removed | Removed. |
469
+ | The instance leaves the DataModel (unparented, destroyed, or an **ancestor** of it unparented) | Removed. CollectionService announces the tag as gone for the whole subtree that left, and announces it again when it is parented back in, so the components come back with it. Moving an instance *within* the DataModel announces nothing and changes nothing. |
470
+ | The component a link names is destroyed | Removed, whatever the streaming mode: that is a lifecycle event, not the tree moving. |
471
+ | Some **other** component on a linked instance is destroyed | **Kept**, including a subclass of the one the link names. |
472
+ | A link attribute is re-pointed at something that fails its guard | Removed, and built again if it is pointed back at something valid. |
473
+ | The instance a **plain** link attribute names stops passing its guard | Removed, and built again once it passes. The guard carries the whole shape, so a linked model losing the child the link asked for counts, whatever the streaming mode: the target's tree is not this component's tree. |
474
+ | The tree under a linked **component** breaks | Depends on that component's own streaming mode, since its tree is its business. Under `Watching` it goes and takes this component with it. Under `Disabled` it stays, and so does this one. |
475
+ | A required link attribute is cleared from outside | Removed. |
476
+ | A plain attribute is changed to a value its guard rejects | Removed, and built again once it is valid. The warning names it: `invalid attribute 'speed' ("fast")`. With a `defaults` entry for it, **kept** with its last good value, and `onAttributeChanged` does not fire. |
477
+ | A child a link names is replaced by another instance of the same name | Removed and built again around the new one, so it never holds a child that has left. |
478
+ | The instance tree stops matching (a child goes, including one a link names) | Follows `streamingMode` (below). |
479
+
480
+ Swaps need a note, because signals are deferred. When a child is parented out and its replacement is
481
+ parented in within one resumption, they arrive as a single change: the child of that name is now a
482
+ different instance. There is never a moment with no child at all. It is still a different tree, so
483
+ the component is still rebuilt.
484
+
485
+ This holds even when the swap happens before Flamework starts watching. `getComponent` builds a
486
+ component the moment you ask for it, but the tag that makes Flamework follow its tree is announced a
487
+ resumption later. If the tree changes in between, Flamework compares it with the child the component
488
+ was actually built with, not with whatever the tree holds when Flamework first looks.
489
+
490
+ The streaming mode only affects the last row, because that is the only row about the tree rather
491
+ than about another object's lifetime:
492
+
493
+ | `ComponentStreamingMode` | A child, or a child link, going away |
494
+ |---|---|
495
+ | `Contextual` (default) | Re-checked on a client, ignored on a server. |
496
+ | `Watching` | Re-checked, so the component goes and comes back with its tree. |
497
+ | `Disabled` | Read once and kept. The component stays, still holding the child it resolved to. |
498
+
499
+ `Disabled` reads the *tree* once, not the components in it. A child that moves elsewhere in the
500
+ DataModel keeps its tag and its components, and that is the case where `Disabled` keeps the
501
+ component. A child that is unparented or destroyed loses its own components on the way out. So a
502
+ link naming one of them falls under the "component a link names is destroyed" row of the previous
503
+ table, not under streaming at all: the owner goes with it whatever the mode.
504
+
505
+ `Disabled` applies to the component that was built, not to the next one. Something else can take
506
+ that component down, say a link attribute re-pointed at an instance its guard refuses. The build
507
+ that follows reads the tree as it is then. So if the tree no longer holds the child a child link
508
+ names, the component stays down until it does. While the component is down, a child link looks its
509
+ child up again whenever a child of that name arrives, and whenever the child it holds loses its
510
+ component. So if a linked child is destroyed and replaced by another that carries the component, the
511
+ component comes back around the new one. This happens under `Disabled`, and under `Contextual` on a
512
+ server.
513
+
514
+ ### Waiting and warnings
515
+
516
+ | Option | Effect |
517
+ |---|---|
518
+ | `warningTimeout` | Seconds before Flamework says what a component is still waiting for, links included. |
519
+ | `attributeWarningTimeout` | Seconds before it says an attribute's instance has not streamed in. Defaults to `warningTimeout`. |
520
+
521
+ ```ts
522
+ @Component({ tag: "Turret", attributeWarningTimeout: 10 })
523
+ ```
524
+
525
+ Both default to 5, and `0` disables them. Keep instances that an attribute names somewhere that is
526
+ always loaded, such as ReplicatedStorage or inside the same model, and the wait never happens.
527
+
528
+ The warning explains why, criterion by criterion, and follows a link into the linked component's own
529
+ reasons:
530
+
531
+ ```
532
+ Waiting for component 'Turret'
533
+ Waiting for the following criteria: instance guard (child 'Barrel' is missing (expected BasePart)),
534
+ child 'EffectHandler' with component 'EffectHandlerComponent' (Workspace.Turret.EffectHandler is
535
+ waiting for: CollectionService tag), attribute 'Owner' with component 'PlayerComponent' (the
536
+ attribute names nothing that has streamed in)
537
+ ```
538
+
539
+ Writing a link attribute reports problems the same way. An instance that could never carry the
540
+ component it names raises an error with that component's reason:
541
+ `did not pass the guard for attribute 'Rig' of 'Turret': instance guard (child 'Root' is missing (expected BasePart))`.
542
+ An instance of the right shape that just has no component yet is refused with a warning, and the
543
+ attribute is left alone.
544
+
545
+ A warning is only ever about something that is **waiting**. A link watches the instance it names
546
+ without waiting for it, so it neither starts a warning nor keeps one going. If you untag an
547
+ instance, the warning goes with the tag, however many links are still watching, and tagging it
548
+ again starts the wait over. The same goes down the chain, in both directions. The components a
549
+ watched component depends on are watched too, and report nothing until something asks for the
550
+ component itself. When the tag that was asking goes, their warnings go with it.
551
+
552
+ The warning also comes again after every loss. A component that goes down (its tree broke, a link
553
+ was lost, an attribute went bad) and stays down for `warningTimeout` seconds warns just like one that
554
+ never came up, with the reason.
555
+
556
+ ## Where components may attach
557
+
558
+ | Option | Effect |
559
+ |---|---|
560
+ | `predicate` | Rejects an instance outright, before anything else runs. |
561
+ | `ancestorWhitelist` | Only construct under these ancestors. Takes priority over the blocklist. |
562
+ | `ancestorBlacklist` | Never construct under these. Defaults to ServerStorage, ReplicatedStorage, StarterPack, StarterGui and StarterPlayer. |
563
+
564
+ ```ts
565
+ @Component({ tag: "Prop", predicate: (instance) => instance.Name !== "Template" })
566
+ ```
567
+
568
+ The default blocklist is why a tagged template in ReplicatedStorage gets no component, while a
569
+ tagged instance cloned into Workspace does.
570
+
571
+ The ancestor lists only gate construction that Flamework drives: a tag, or a link to the component.
572
+ So you can still attach a component by hand to something in ReplicatedStorage. The `predicate` also
573
+ gates the eager path in `getComponent`: an instance it rejects never gets a component unless you call
574
+ `addComponent` yourself. `addComponent` ignores all three options.
575
+
576
+ A component can also be tied to the build's scopes with `activeIn` and `inactiveIn`, on the
577
+ decorator or on the registration (`registerComponent(Class, { ... })`, `fromPath(path, { ... })`).
578
+ A component left out by scope is not registered in the plugin at all. It is never attached, and
579
+ `getComponent` on it raises an error with the reason. See [Scopes](11-scopes.md).
580
+
581
+ When the condition on `fromPath`, `fromGlob`, `registerComponents` or `registerComponentsGlob` does
582
+ not hold, the folder is not loaded at all, so it can be missing from the place. The error from
583
+ `getComponent` then names that registration. A condition on `includePlugin` does not stop
584
+ `fromPath` from loading its folder: `fromPath` looks the folder up when you call it.
585
+
586
+ ## Streaming
587
+
588
+ With StreamingEnabled, an instance can arrive before its descendants. So an instance tree that
589
+ requires children may be incomplete at first, and complete a moment later.
590
+
591
+ | `ComponentStreamingMode` | Behaviour |
592
+ |---|---|
593
+ | `Contextual` (default) | Watches on the client; never on the server; skips atomic models, which replicate whole. |
594
+ | `Watching` | Always follows the tree as it changes. |
595
+ | `Disabled` | Reads the tree once. |
596
+
597
+ ```ts
598
+ @Component({ tag: "Character", streamingMode: ComponentStreamingMode.Watching })
599
+ ```
600
+
601
+ Watching uses one watcher per required child. It does not re-check the whole tree. For
602
+ `Model & { Root: Part & { Texture: Texture } }`, it listens for children arriving and leaving on the
603
+ model and, once `Root` has resolved, on `Root`. With `watchRenames` on (below), it also follows the
604
+ `Name` of each resolved child. A `Texture` arriving three levels down re-resolves only that one
605
+ slot. A child that is not in the tree, or a second child with a required name, changes nothing
606
+ however often it moves.
607
+
608
+ Renames are not followed unless the component asks for it with `watchRenames: true` (or
609
+ `components.watchRenames` in `flamework.config.json`). A child is rarely renamed, and only the
610
+ renamed child announces a rename. So following names costs a connection on each resolved child.
611
+ While a required child is missing, it also costs one on every other child, since that is the only
612
+ way to hear a sibling being renamed to the required name. Once the required child resolves, the
613
+ children ahead of it in child order stay followed: `FindFirstChild` reads the first child of a name,
614
+ so one of them renamed to that name becomes the one read.
615
+
616
+ With `watchRenames` left off, a rename is noticed only the next time that slot is read. That covers
617
+ a child renamed away, and a sibling renamed to a required name. The slot is read when a child of
618
+ that name arrives, when the child it resolved to leaves, or when the tag arrives.
619
+
620
+ A guard written by hand with `instanceGuard` has no such structure. It is re-run whole on every
621
+ descendant change, and hears no rename at all.
622
+
623
+ When a watched component's tree breaks apart again, the component is removed. That holds however the
624
+ component came to qualify. For example, a tag arriving at an instance that another component's link
625
+ was already watching re-reads the tree, and what it reads is what is watched from then on. So the
626
+ component still goes and comes back with its tree afterwards.
627
+
628
+ If an instance never qualifies, Flamework warns after `warningTimeout` seconds (default 5, `0`
629
+ disables) and lists the criteria it is still waiting on, such as
630
+ `instance guard (child 'Root.Texture' is missing (expected Texture))`. This is usually the fastest
631
+ way to find a typo in a tag or a missing child.
632
+
633
+ ## Component dependencies
634
+
635
+ A component can depend on another component **on the same instance**. Declare it as a constructor
636
+ parameter. `ComponentMetadata` has to come first, because `BaseComponent` takes it:
637
+
638
+ ```ts
639
+ import { BaseComponent, Component, ComponentMetadata } from "@flamework-experimental/components";
640
+
641
+ @Component({ tag: "Car" })
642
+ export class Car extends BaseComponent<{}, Model> {
643
+ constructor(
644
+ metadata: ComponentMetadata,
645
+ private engine: Engine,
646
+ ) {
647
+ super(metadata);
648
+ }
649
+ }
650
+ ```
651
+
652
+ `Car` is not constructed until `Engine` exists on the same instance, in either tag order. This is
653
+ the same criteria mechanism that streaming uses. Some `Engine`s Flamework never builds by itself: one
654
+ with no tag, or one its predicate refuses on this instance. Such an `Engine` counts once you add it
655
+ with `addComponent`. Removing an `Engine` by hand takes `Car` down with it, tagged or not. `Car`
656
+ comes back with the next `Engine`: one you add, or one that `getComponent` builds on the
657
+ still-tagged instance.
658
+
659
+ ## Working with components
660
+
661
+ `Components` is a provider exported by the plugin, so inject it:
662
+
663
+ ```ts
664
+ @Provider()
665
+ export class VehicleService {
666
+ constructor(private components: Components) {}
667
+ }
668
+ ```
669
+
670
+ | Method | Notes |
671
+ |---|---|
672
+ | `getComponent<T>(instance)` | Exact class only. Constructs eagerly if the instance qualifies. |
673
+ | `getComponents<T>(instance)` | Every component on the instance matching a class **or interface**. |
674
+ | `getAllComponents<T>()` | The same, across every instance. |
675
+ | `addComponent<T>(instance)` | Attaches by hand. Throws if the guards fail. |
676
+ | `removeComponent<T>(instance)` | Detaches and destroys. |
677
+ | `waitForComponent<T>(instance)` | Promise; resolves immediately if it already exists. A waiter whose handler removes the component leaves the others waiting for the next one. |
678
+ | `onComponentAdded<T>(cb)` | Fires for every future component of that type. |
679
+ | `onComponentRemoved<T>(cb)` | Fires after the component has left the lookups: before `destroy` where the place delivers signals immediately, after it where they are deferred. |
680
+
681
+ `getComponent` needs the exact class. The polymorphic ones (`getComponents`, `getAllComponents`,
682
+ and both listeners) accept an interface the component implements, or a superclass that is itself
683
+ decorated with `@Component()` (an abstract base with no `tag`, say). The ids a component answers to
684
+ are read from Flamework's metadata, which only a decorated class carries. So a base class without
685
+ its own decorator is not looked up: asking for it finds nothing, and so does asking for an
686
+ interface only such a base class declares. The same rule decides which `implements` clauses count
687
+ for lifecycle events ([Lifecycle events](04-lifecycle-events.md#the-events)).
688
+
689
+ ```ts
690
+ // every component on this instance that implements OnTick
691
+ for (const component of this.components.getComponents<OnTick>(instance)) {
692
+ component.onTick(dt);
693
+ }
694
+ ```
695
+
696
+ ## Patterns
697
+
698
+ **Server and client components with the same tag.** Register different classes from the two entry
699
+ points. They attach to the same instances and never see each other.
700
+
701
+ **An interface for shared behaviour.** Declare `interface Damageable { takeDamage(n): void }`,
702
+ implement it on several components, and use `getComponents<Damageable>(instance)` to reach whichever
703
+ are present.
704
+
705
+ **`waitForComponent` at boundaries.** When a provider needs a component that may not exist yet,
706
+ `await this.components.waitForComponent<Vehicle>(instance)` is better than polling.
707
+
708
+ **Composition over inheritance.** Two components on one instance with a dependency between them are
709
+ usually clearer than a deep component hierarchy. It is also the case Flamework's tracker is built
710
+ for.
711
+
712
+ ## Caveats
713
+
714
+ - **`getComponent` constructs.** It is not a pure lookup. If the instance is in the DataModel,
715
+ tagged, passes the predicate and qualifies, `getComponent` builds the component then and there,
716
+ ignoring the ancestor lists. The instance must be in the DataModel because that is what announces
717
+ the tag: an instance sitting in a pool, or a template being assembled, gets nothing until it is
718
+ parented in. Whether some other component's link happens to be watching that instance makes no
719
+ difference to the answer. That includes a link that watched it while its instance guard was still
720
+ failing. It also includes an instance under a blocked ancestor, where `getComponent` is the only
721
+ way in and a link watching it is not allowed to build the component there. This stays true as the
722
+ tree goes on moving: a watched component still comes and goes with its tree on an instance a link
723
+ found first. Use `getAllComponents` when you want to *observe* rather than ensure.
724
+ - **`getComponent` returns nothing for a component that is still constructing.** So a constructor
725
+ asking for its own component gets `undefined`. So does an attribute-changed handler that runs
726
+ inside the write of one of the component's `defaults`, where the place delivers signals
727
+ immediately. Forcing the construction with `addComponent` from inside the constructor raises
728
+ `component '...' is cyclic`.
729
+ - **Extinguishing the module destroys every component** and stops watching the tags. `addComponent`
730
+ on the extinguished module raises, and tagging an instance afterwards does nothing. `getComponent`
731
+ builds nothing from the moment the extinguish begins: it answers `undefined` for a still-tagged
732
+ instance.
733
+ - **A `destroy` that raises does not hold up the teardown.** Whatever Flamework attached for the
734
+ component (the attribute-changed connections behind `onAttributeChanged`) is released either way,
735
+ so nothing keeps firing into a component that has gone.
736
+ - On extinguish, the failure is warned about and the remaining components still come down, so one
737
+ component cannot leave a module half-extinguished.
738
+ - A removal Flamework makes on its own (a tree, a link, an attribute or a dependency lost) warns
739
+ the same way, and the change that caused it still finishes. For example, a dependent whose
740
+ `destroy` raises does not keep its dependency attached after the dependency's tag has gone.
741
+ - A `removeComponent` you call by hand still re-raises the error, since you asked for the removal.
742
+ - **Per-frame events come from the module's lifecycle plugin.** `disableDefaultLifecycle()` on the
743
+ module that includes `ComponentPlugin` stops components ticking. `onStart` still runs.
744
+ - **An invalid attribute throws** unless a default is configured.
745
+ - **`onComponentRemoved` runs when the engine delivers it.** The announcement is a BindableEvent, so
746
+ when the callback runs depends on how the place delivers signals:
747
+ - **Immediately.** The callback runs inside the removal, before `destroy`, so the component is
748
+ still usable inside it. But it has already left `getComponent` and `getComponents` by then, and
749
+ nothing builds a replacement while the removal is running. So the value the callback is handed
750
+ is the only way to reach it. An `addComponent` of your own from the callback still attaches a
751
+ new one, and the dependents holding the old one are rebuilt around it once the removal is over.
752
+ - **Deferred.** The callback runs once the thread yields. The removal has finished and `destroy`
753
+ has already run, and asking a still-tagged instance for the component there builds a new one.
754
+
755
+ A removal by hand leaves the tag alone. So asking a still-tagged instance for the component
756
+ *after* `removeComponent` has returned builds a new one, as it always has. That is also what keeps
757
+ two components whose links name each other from removing one another twice: taking one down takes
758
+ the other with it, once each.
759
+
760
+ The announcement is delivered a resumption late, so a link compares it with the instance as it
761
+ stands when it arrives. Sometimes the instance has since replaced the component, because that same
762
+ resumption asked for it again. Then the removal leaves the link alone, and updates
763
+ `childComponents` and `attributeComponents` to the component that is there now. Otherwise a cycle
764
+ would remove and rebuild itself for as long as the place is running.
765
+ - **Attribute tracking is on by default.** `refreshAttributes: false` disables `onAttributeChanged`
766
+ as well as the tracking. It does not turn off the watch on the attributes' validity: an attribute
767
+ changed to a value its guard rejects still takes the component down.
768
+ - **A component with no `tag` can only be added by hand.**
769
+ - **`@Component` classes are not providers.** They are not picked up by `registerProviders`, and
770
+ `registerComponents` will not pick up providers. `Dependency<T>()` and a provider's constructor
771
+ cannot take one either: the transformer refuses both. Get it from `Components` instead.
772
+ - **Component dependencies are same-instance only.** There is no cross-instance dependency. A link
773
+ is how you reach another instance.
774
+ - **A linked component must be registered in the same module**, by any of its component plugins.
775
+ Igniting raises if it is not.
776
+ - **`addComponent` will not wait.** By hand, a link that has not resolved raises instead of
777
+ yielding. Through a tag, the component is not created until the link has resolved.
778
+ - **A link is only kept current for a tag-driven component.** One added by hand is resolved once,
779
+ like its instance guard.
780
+ - **`refreshAttributes: false` freezes link attributes too.** Re-pointing one stops updating
781
+ `this.attributes` and `attributeComponents`, and `onAttributeChanged` does not fire for them
782
+ either. Only the component's *view* is frozen, not the criterion behind it: a re-point that the
783
+ guard refuses still takes the component down, as it would with tracking on. The component's own
784
+ writes still land, in both `this.attributes` and `attributeComponents`, and, as for a plain
785
+ attribute, they announce nothing.
786
+ - **Clearing a required link raises.** Only an optional one can be set back to `undefined`.
787
+ - **A write that fails its guard raises**, so an attribute never holds a value its type forbids.
788
+ - **A link write to an instance without the component warns and is refused**, rather than raising or
789
+ destroying the component that wrote it. Await the component first.
790
+
791
+ ---
792
+
793
+ Previous: [Lifecycle events](04-lifecycle-events.md) · Next: [Networking](06-networking.md)