@axi-engine/fields 0.1.0

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 ADDED
@@ -0,0 +1,24 @@
1
+ # @axi-engine/fields
2
+
3
+ [![NPM version](https://img.shields.io/npm/v/@axi-engine/utils.svg)](https://www.npmjs.com/package/@axi-engine/fields)
4
+
5
+
6
+ A hierarchical, reactive state management library for Axi Engine, built on top of Preact Signals.
7
+
8
+ ## Description
9
+
10
+ This package provides the core logic for creating,
11
+ managing, and observing the game state.
12
+ It allows you to structure your data in a tree-like hierarchy (FieldTree),
13
+ where each piece of data (Field) is a reactive signal.
14
+ This is the central hub for all persistent game data, such as player stats, inventory, quest flags, and more.
15
+
16
+
17
+
18
+ ```bash
19
+ npm install @axi-engine/fields
20
+ ```
21
+
22
+ ## API Reference
23
+
24
+ Will be available when code and repository will be fully published
@@ -0,0 +1,356 @@
1
+ import { ReadonlySignal, Signal } from '@preact/signals-core';
2
+ import { AxiEventEmitter } from '@axi-engine/events';
3
+ import { PathType } from '@axi-engine/utils';
4
+
5
+ declare const enum FieldsNodeType {
6
+ fieldTree = "FieldTree",
7
+ fields = "Fields"
8
+ }
9
+
10
+ interface FieldPolicy<T> {
11
+ readonly id: string;
12
+ apply: (val: T) => T;
13
+ destroy?: () => void;
14
+ }
15
+ declare class ClampPolicy implements FieldPolicy<number> {
16
+ min: number;
17
+ max: number;
18
+ static readonly id = "clamp";
19
+ readonly id = "clamp";
20
+ constructor(min: number, max: number);
21
+ apply(val: number): number;
22
+ updateBounds(min: number, max: number): void;
23
+ }
24
+ declare class ClampMinPolicy implements FieldPolicy<number> {
25
+ min: number;
26
+ static readonly id = "clampMin";
27
+ readonly id = "clampMin";
28
+ constructor(min: number);
29
+ apply(val: number): number;
30
+ updateBounds(min: number): void;
31
+ }
32
+ declare class ClampMaxPolicy implements FieldPolicy<number> {
33
+ max: number;
34
+ static readonly id = "clampMax";
35
+ readonly id = "clampMax";
36
+ constructor(max: number);
37
+ apply(val: number): number;
38
+ updateBounds(max: number): void;
39
+ }
40
+ declare function clampPolicy(min: number, max: number): ClampPolicy;
41
+ declare function clampMinPolicy(min: number): ClampMinPolicy;
42
+ declare function clampMaxPolicy(max: number): ClampMaxPolicy;
43
+
44
+ /**
45
+ * A reactive state container that wraps a value, making it observable through a Preact Signal.
46
+ * It allows applying a pipeline of transformation or validation "policies" before any new value is set.
47
+ *
48
+ * @template T The type of the value this field holds.
49
+ *
50
+ */
51
+ declare class Field<T> {
52
+ private readonly policies;
53
+ private readonly _val;
54
+ /** A unique identifier for the field. */
55
+ name: string;
56
+ /**
57
+ * Creates an instance of a Field.
58
+ * @param name A unique identifier for the field.
59
+ * @param initialVal The initial value of the field.
60
+ * @param options Optional configuration for the field.
61
+ * @param options.policies An array of policies to apply to the field's value on every `set` operation.
62
+ */
63
+ constructor(name: string, initialVal: T, options?: {
64
+ policies?: FieldPolicy<T>[];
65
+ });
66
+ /**
67
+ * Gets the current raw value of the field.
68
+ * For reactive updates, it's recommended to use the `.signal` property instead.
69
+ */
70
+ get val(): T;
71
+ /**
72
+ * Provides readonly access to the underlying Preact Signal.
73
+ * Subscribe to this signal to react to value changes.
74
+ */
75
+ get signal(): ReadonlySignal<T>;
76
+ /**
77
+ * Sets a new value for the field.
78
+ * The provided value will be processed by all registered policies before the underlying signal is updated.
79
+ * @param val The new value to set.
80
+ */
81
+ set(val: T): void;
82
+ /**
83
+ * Retrieves a specific policy instance by its ID.
84
+ * Useful for accessing a policy's internal state or methods.
85
+ * @template P The expected type of the policy.
86
+ * @param id The unique ID of the policy to retrieve.
87
+ * @returns The policy instance, or `undefined` if not found.
88
+ */
89
+ getPolicy<P extends FieldPolicy<T>>(id: string): P | undefined;
90
+ /**
91
+ * Adds a new policy to the field or replaces an existing one with the same ID.
92
+ * The new policy will be applied on the next `set()` operation.
93
+ * If a policy with the same ID already exists, its `destroy` method will be called before it is replaced.
94
+ * @param policy The policy instance to add.
95
+ */
96
+ addPolicy(policy: FieldPolicy<T>): void;
97
+ /**
98
+ * Removes a policy from the field by its ID and call `destroy` method.
99
+ * @param policyId The unique ID of the policy to remove.
100
+ * @returns `true` if the policy was found and removed, otherwise `false`.
101
+ */
102
+ removePolicy(policyId: string): boolean;
103
+ /**
104
+ * Removes all policies from the field.
105
+ * After this, `set()` will no longer apply any transformations to the value until new policies are added.
106
+ */
107
+ clearPolicies(): void;
108
+ /**
109
+ * Forces the current value to be re-processed by all policies.
110
+ * Useful if a policy's logic has changed and you need to re-evaluate the current state.
111
+ */
112
+ reapplyPolicies(): void;
113
+ /**
114
+ * Cleans up resources used by the field and its policies.
115
+ * This should be called when the field is no longer needed to prevent memory leaks from reactive policies.
116
+ */
117
+ destroy(): void;
118
+ }
119
+
120
+ interface NumberFieldOptions {
121
+ min?: number;
122
+ max?: number;
123
+ policies?: FieldPolicy<number>[];
124
+ }
125
+ declare class NumberField extends Field<number> {
126
+ get min(): number | undefined;
127
+ get max(): number | undefined;
128
+ get isMin(): boolean;
129
+ get isMax(): boolean;
130
+ constructor(name: string, initialVal: number, options?: NumberFieldOptions);
131
+ inc(amount?: number): void;
132
+ dec(amount?: number): void;
133
+ }
134
+
135
+ /**
136
+ * An abstract base class for managing a reactive collection of `Field` instances.
137
+ *
138
+ * This class is designed to be the foundation for state management systems,
139
+ * such as managing stats, flags, or items.
140
+ *
141
+ * @template T The common base type for the values held by the fields in this collection.
142
+ */
143
+ declare abstract class BaseFields<T> {
144
+ protected readonly _fields: Signal<Map<string, Field<T>>>;
145
+ readonly events: AxiEventEmitter<"created" | "removed">;
146
+ /**
147
+ * A readonly signal providing access to the current map of fields.
148
+ * Use this signal with `effect` to react when fields are added or removed from the collection.
149
+ * Avoid to change any data in the map manually.
150
+ */
151
+ get fields(): ReadonlySignal<ReadonlyMap<string, Field<any>>>;
152
+ /**
153
+ * Checks if a field with the given name exists in the collection.
154
+ * @param name The name of the field to check.
155
+ * @returns `true` if the field exists, otherwise `false`.
156
+ */
157
+ has(name: string): boolean;
158
+ /**
159
+ * Creates and adds a new `Field` to the collection.
160
+ * @param name The unique name for the new field.
161
+ * @param initialValue The initial value for the new field.
162
+ * @returns The newly created `Field` instance.
163
+ */
164
+ create(name: string, initialValue: T): Field<T>;
165
+ /**
166
+ * Adds a pre-existing `Field` instance to the collection.
167
+ * Throws an error if a field with the same name already exists.
168
+ * @param field The `Field` instance to add.
169
+ * @returns The added `Field` instance.
170
+ */
171
+ add(field: Field<T>): Field<T>;
172
+ /**
173
+ * Retrieves a field by its name.
174
+ * Throws an error if the field does not exist.
175
+ * @param name The name of the field to retrieve.
176
+ * @returns The `Field` instance.
177
+ */
178
+ get(name: string): Field<T>;
179
+ /**
180
+ * "Update or Insert": Updates a field's value if it exists, or creates a new one if it doesn't.
181
+ * @param name The name of the field.
182
+ * @param value The value to set.
183
+ * @returns The existing or newly created `Field` instance.
184
+ */
185
+ upset(name: string, value: T): Field<T>;
186
+ /**
187
+ * Removes one or more fields from the collection.
188
+ * This method ensures that the `destroy` method of each removed field is called to clean up its resources.
189
+ * @param names A single name or an array of names to remove.
190
+ */
191
+ remove(names: string | string[]): void;
192
+ /**
193
+ * Removes all fields from the collection, ensuring each is properly destroyed.
194
+ */
195
+ clear(): void;
196
+ /**
197
+ * Creates a serializable snapshot of the current state of all fields.
198
+ * @returns A plain JavaScript object representing the values of all fields.
199
+ */
200
+ snapshot(): Record<string, any>;
201
+ /**
202
+ * Restores the state of the fields from a snapshot.
203
+ * It uses the `upset` logic to create or update fields based on the snapshot data.
204
+ * @param snapshot The snapshot object to load.
205
+ */
206
+ hydrate(snapshot: any): void;
207
+ }
208
+
209
+ declare class Fields extends BaseFields<any> {
210
+ createNumber(name: string, initialValue: number, options?: NumberFieldOptions): NumberField;
211
+ upsetNumber(name: string, value: number, options?: NumberFieldOptions): NumberField;
212
+ getNumber(name: string): NumberField;
213
+ create<T>(name: string, initialValue: T): Field<T>;
214
+ upset<T>(name: string, value: T): Field<T>;
215
+ get<T>(name: string): Field<T>;
216
+ }
217
+
218
+ declare class TypedFields<T> extends BaseFields<T> {
219
+ }
220
+
221
+ /** A type alias for any container that can be a child node in a FieldTree */
222
+ type TreeOrFieldsContainer = FieldTree | Fields | TypedFields<any>;
223
+ /** Describes the payload for events emitted when a container is created or removed from a FieldTree. */
224
+ type FieldTreeContainerEvent = {
225
+ type: 'created' | 'removed';
226
+ name: string;
227
+ path: [];
228
+ node: TreeOrFieldsContainer;
229
+ };
230
+ /**
231
+ * Represents the global, persistent state of the entire game.
232
+ * This service acts as the single source of truth for long-term data that exists
233
+ * across different scenes and scripts, such as player stats, inventory,
234
+ * and overall game progress.
235
+ * It is designed to be the foundational data layer,
236
+ * independent of any single script's / minigames execution lifecycle.
237
+ *
238
+ * @todo:
239
+ * - add node removing
240
+ */
241
+ declare class FieldTree {
242
+ private readonly _items;
243
+ readonly events: AxiEventEmitter<"created" | "removed">;
244
+ /**
245
+ * A readonly signal providing access to the map of child nodes.
246
+ * Use this with `effect` to react to structural changes in the tree (e.g., adding a new `Fields` container).
247
+ */
248
+ get items(): ReadonlySignal<ReadonlyMap<string, TreeOrFieldsContainer>>;
249
+ constructor();
250
+ /**
251
+ * Checks if a path to a node or fields container or field exists without creating it.
252
+ * @returns true if the entire path exists, false otherwise.
253
+ */
254
+ hasPath(path: PathType): boolean;
255
+ /**
256
+ * Retrieves a child node and asserts that it is an instance of `FieldTree`.
257
+ * @param name The name of the child node.
258
+ * @returns The `FieldTree` instance.
259
+ * @throws If the node does not exist or is not a `FieldTree`.
260
+ */
261
+ getFieldTree(name: string): FieldTree;
262
+ /**
263
+ * Retrieves a child node and asserts that it is an instance of `Fields`.
264
+ * @param name The name of the child node.
265
+ * @returns The `Fields` instance.
266
+ * @throws If the node does not exist or is not a `Fields` container.
267
+ */
268
+ getFields(name: string): Fields;
269
+ /**
270
+ * Retrieves a child node and asserts that it is an instance of `TypedFields`.
271
+ * @param name The name of the child node.
272
+ * @returns The `TypedFields` instance.
273
+ * @throws If the node does not exist or is not a `TypedFields` container.
274
+ */
275
+ getTypedFields<T>(name: string): TypedFields<T>;
276
+ /**
277
+ * Retrieves a child node from this tree level without type checking.
278
+ * @param name The name of the child node.
279
+ * @returns The retrieved node, which can be a `FieldTree` or a `Fields` container.
280
+ * @throws If a node with the given name cannot be found.
281
+ */
282
+ getNode(name: string): TreeOrFieldsContainer;
283
+ /**
284
+ * Creates and adds a new `FieldTree` node as a child of this one.
285
+ * @param name The unique name for the new `FieldTree` node.
286
+ * @returns The newly created `FieldTree` instance.
287
+ */
288
+ createFieldTree(name: string): FieldTree;
289
+ /**
290
+ * Creates and adds a new `Fields` container as a child of this one.
291
+ * @param name The unique name for the new `Fields` container.
292
+ * @returns The newly created `Fields` instance.
293
+ */
294
+ createFields(name: string): Fields;
295
+ /**
296
+ * Creates and adds a new `TypedFields` container as a child of this one.
297
+ * @param name The unique name for the new `TypedFields` container.
298
+ * @returns The newly created `TypedFields` instance.
299
+ */
300
+ createTypedFields<T>(name: string): TypedFields<T>;
301
+ /**
302
+ * Navigates through the tree using a path and returns the `Fields` container at the end.
303
+ * @param path The path to the `Fields` container (e.g., 'player/stats').
304
+ * @returns The `Fields` container at the specified path.
305
+ * @throws If the path is empty, or any intermediate node is not a `FieldTree`.
306
+ */
307
+ getFieldsByPath(path: PathType): Fields;
308
+ /**
309
+ * Creates a `Field` at a deeply nested path.
310
+ * The last part of the path is treated as the field name, and the preceding parts as the path to its container.
311
+ * @param path The full path to the new field (e.g., 'player/stats/health').
312
+ * @param initialValue The initial value for the new field.
313
+ * @returns The newly created `Field` instance.
314
+ */
315
+ create<T>(path: PathType, initialValue: T): Field<T>;
316
+ /**
317
+ * Creates a `NumberField` at a deeply nested path.
318
+ * @param path The full path to the new field (e.g., 'player/stats/mana').
319
+ * @param initialValue The initial numeric value.
320
+ * @returns The newly created `NumberField` instance.
321
+ */
322
+ createNumber(path: PathType, initialValue: number): NumberField;
323
+ /**
324
+ * Retrieves a `Field` from a deeply nested path.
325
+ * @param path The full path to the field (e.g., 'player/stats/name').
326
+ * @returns The `Field` instance at the specified path.
327
+ */
328
+ get<T>(path: PathType): Field<T>;
329
+ /**
330
+ * Retrieves a `NumberField` from a deeply nested path.
331
+ * @param path The full path to the number field (e.g., 'player/stats/level').
332
+ * @returns The `NumberField` instance at the specified path.
333
+ */
334
+ getNumber(path: PathType): NumberField;
335
+ /**
336
+ * Creates a serializable snapshot of the entire tree and its contained fields.
337
+ * @returns A plain JavaScript object representing the complete state managed by this tree.
338
+ */
339
+ snapshot(): Record<string, any>;
340
+ /**
341
+ * Restores the state of the tree from a snapshot.
342
+ * It intelligently creates missing nodes based on `__type` metadata and delegates hydration to child nodes.
343
+ * @param snapshot The snapshot object to load.
344
+ */
345
+ hydrate(snapshot: any): void;
346
+ /**
347
+ * @private
348
+ * Generic internal method for creating and adding a new node to the tree.
349
+ * @param name The name of the node to create.
350
+ * @param ctor The constructor for the node type (e.g., `FieldTree` or `Fields`).
351
+ * @returns The newly created node instance.
352
+ */
353
+ private createNode;
354
+ }
355
+
356
+ export { BaseFields, ClampMaxPolicy, ClampMinPolicy, ClampPolicy, Field, type FieldPolicy, FieldTree, type FieldTreeContainerEvent, Fields, FieldsNodeType, NumberField, type NumberFieldOptions, type TreeOrFieldsContainer, TypedFields, clampMaxPolicy, clampMinPolicy, clampPolicy };