joist-core 2.4.0-next.2 → 2.4.0-next.21
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/build/EntityManager.cjs +25 -11
- package/build/EntityManager.cjs.map +1 -1
- package/build/EntityManager.d.cts +11 -4
- package/build/EntityManager.d.cts.map +1 -1
- package/build/EntityManager.d.mts +11 -4
- package/build/EntityManager.d.mts.map +1 -1
- package/build/EntityManager.js +25 -13
- package/build/EntityManager.js.map +1 -1
- package/build/EntityMetadata.cjs +0 -1
- package/build/EntityMetadata.cjs.map +1 -1
- package/build/EntityMetadata.d.cts +8 -4
- package/build/EntityMetadata.d.cts.map +1 -1
- package/build/EntityMetadata.d.mts +8 -4
- package/build/EntityMetadata.d.mts.map +1 -1
- package/build/EntityMetadata.js +0 -1
- package/build/EntityMetadata.js.map +1 -1
- package/build/IndexManager.cjs +23 -5
- package/build/IndexManager.cjs.map +1 -1
- package/build/IndexManager.d.cts.map +1 -1
- package/build/IndexManager.d.mts.map +1 -1
- package/build/IndexManager.js +23 -5
- package/build/IndexManager.js.map +1 -1
- package/build/batchloaders/populateBatchLoader.cjs +2 -2
- package/build/batchloaders/populateBatchLoader.js +2 -2
- package/build/batchloaders/recursiveChildrenBatchLoader.cjs +1 -1
- package/build/batchloaders/recursiveChildrenBatchLoader.js +1 -1
- package/build/batchloaders/recursiveM2mBatchLoader.cjs +1 -1
- package/build/batchloaders/recursiveM2mBatchLoader.js +1 -1
- package/build/batchloaders/recursiveParentsBatchLoader.cjs +1 -1
- package/build/batchloaders/recursiveParentsBatchLoader.js +1 -1
- package/build/config.cjs +74 -1
- package/build/config.cjs.map +1 -1
- package/build/config.d.cts +83 -2
- package/build/config.d.cts.map +1 -1
- package/build/config.d.mts +83 -2
- package/build/config.d.mts.map +1 -1
- package/build/config.js +74 -1
- package/build/config.js.map +1 -1
- package/build/configure.cjs +6 -2
- package/build/configure.cjs.map +1 -1
- package/build/configure.d.cts.map +1 -1
- package/build/configure.d.mts.map +1 -1
- package/build/configure.js +6 -2
- package/build/configure.js.map +1 -1
- package/build/dataloaders/findDataLoader.cjs +1 -1
- package/build/dataloaders/findDataLoader.js +1 -1
- package/build/dataloaders/findOrCreateDataLoader.cjs +6 -2
- package/build/dataloaders/findOrCreateDataLoader.cjs.map +1 -1
- package/build/dataloaders/findOrCreateDataLoader.d.cts.map +1 -1
- package/build/dataloaders/findOrCreateDataLoader.d.mts.map +1 -1
- package/build/dataloaders/findOrCreateDataLoader.js +7 -3
- package/build/dataloaders/findOrCreateDataLoader.js.map +1 -1
- package/build/dataloaders/findPaginatedDataLoader.cjs +1 -1
- package/build/dataloaders/findPaginatedDataLoader.js +1 -1
- package/build/defaults.cjs +2 -2
- package/build/defaults.js +2 -2
- package/build/fields.cjs +1 -0
- package/build/fields.cjs.map +1 -1
- package/build/fields.d.cts.map +1 -1
- package/build/fields.d.mts.map +1 -1
- package/build/fields.js +1 -0
- package/build/fields.js.map +1 -1
- package/build/index.cjs +13 -8
- package/build/index.cjs.map +1 -1
- package/build/index.d.cts +4 -3
- package/build/index.d.cts.map +1 -1
- package/build/index.d.mts +4 -3
- package/build/index.d.mts.map +1 -1
- package/build/index.js +13 -10
- package/build/index.js.map +1 -1
- package/build/json.cjs +3 -3
- package/build/json.js +3 -3
- package/build/loading/loadHints.cjs +1 -1
- package/build/loading/loadHints.js +1 -1
- package/build/loading/withLoaded.cjs +2 -2
- package/build/loading/withLoaded.js +2 -2
- package/build/newTestInstance.cjs +1 -0
- package/build/newTestInstance.cjs.map +1 -1
- package/build/newTestInstance.d.cts.map +1 -1
- package/build/newTestInstance.d.mts.map +1 -1
- package/build/newTestInstance.js +1 -0
- package/build/newTestInstance.js.map +1 -1
- package/build/preloading/partitionHint.cjs +1 -1
- package/build/preloading/partitionHint.js +1 -1
- package/build/queries/sql/execute.cjs +68 -89
- package/build/queries/sql/execute.cjs.map +1 -1
- package/build/queries/sql/execute.d.cts +35 -18
- package/build/queries/sql/execute.d.cts.map +1 -1
- package/build/queries/sql/execute.d.mts +35 -18
- package/build/queries/sql/execute.d.mts.map +1 -1
- package/build/queries/sql/execute.js +70 -91
- package/build/queries/sql/execute.js.map +1 -1
- package/build/queries/sql/query.cjs +77 -30
- package/build/queries/sql/query.cjs.map +1 -1
- package/build/queries/sql/query.d.cts +64 -21
- package/build/queries/sql/query.d.cts.map +1 -1
- package/build/queries/sql/query.d.mts +64 -21
- package/build/queries/sql/query.d.mts.map +1 -1
- package/build/queries/sql/query.js +75 -30
- package/build/queries/sql/query.js.map +1 -1
- package/build/reactivity/ReactionsManager.cjs +168 -2
- package/build/reactivity/ReactionsManager.cjs.map +1 -1
- package/build/reactivity/ReactionsManager.d.cts +54 -1
- package/build/reactivity/ReactionsManager.d.cts.map +1 -1
- package/build/reactivity/ReactionsManager.d.mts +54 -1
- package/build/reactivity/ReactionsManager.d.mts.map +1 -1
- package/build/reactivity/ReactionsManager.js +169 -3
- package/build/reactivity/ReactionsManager.js.map +1 -1
- package/build/reactivity/reactiveHints.cjs +3 -3
- package/build/reactivity/reactiveHints.cjs.map +1 -1
- package/build/reactivity/reactiveHints.d.cts.map +1 -1
- package/build/reactivity/reactiveHints.d.mts.map +1 -1
- package/build/reactivity/reactiveHints.js +3 -3
- package/build/reactivity/reactiveHints.js.map +1 -1
- package/build/relations/AsyncReactiveField.cjs +3 -3
- package/build/relations/AsyncReactiveField.js +3 -3
- package/build/relations/LazyField.cjs +14 -0
- package/build/relations/LazyField.cjs.map +1 -1
- package/build/relations/LazyField.d.cts +6 -0
- package/build/relations/LazyField.d.cts.map +1 -1
- package/build/relations/LazyField.d.mts +6 -0
- package/build/relations/LazyField.d.mts.map +1 -1
- package/build/relations/LazyField.js +14 -0
- package/build/relations/LazyField.js.map +1 -1
- package/build/relations/ReactiveField.cjs +2 -2
- package/build/relations/ReactiveField.js +2 -2
- package/build/relations/ReactiveManyToMany.cjs +1 -1
- package/build/relations/ReactiveManyToMany.js +1 -1
- package/build/relations/ReactiveReference.cjs +1 -1
- package/build/relations/ReactiveReference.js +1 -1
- package/build/relations/RecursiveCollection.cjs +3 -3
- package/build/relations/RecursiveCollection.js +3 -3
- package/build/relations/hasProperty.cjs +2 -2
- package/build/relations/hasProperty.js +2 -2
- package/build/relations/index.cjs +5 -5
- package/build/relations/index.js +5 -5
- package/build/resurrection.cjs +11 -1
- package/build/resurrection.cjs.map +1 -1
- package/build/resurrection.d.cts +4 -1
- package/build/resurrection.d.cts.map +1 -1
- package/build/resurrection.d.mts +4 -1
- package/build/resurrection.d.mts.map +1 -1
- package/build/resurrection.js +11 -2
- package/build/resurrection.js.map +1 -1
- package/build/transitions.cjs +89 -0
- package/build/transitions.cjs.map +1 -0
- package/build/transitions.d.cts +110 -0
- package/build/transitions.d.cts.map +1 -0
- package/build/transitions.d.mts +110 -0
- package/build/transitions.d.mts.map +1 -0
- package/build/transitions.js +84 -0
- package/build/transitions.js.map +1 -0
- package/package.json +2 -2
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_utils = require("../utils.cjs");
|
|
2
3
|
const require_reactivity_reactiveHints = require("./reactiveHints.cjs");
|
|
4
|
+
const require_rules = require("../rules.cjs");
|
|
5
|
+
const require_transitions = require("../transitions.cjs");
|
|
3
6
|
const require_EntityMetadata = require("../EntityMetadata.cjs");
|
|
4
7
|
const require_trusted = require("../trusted.cjs");
|
|
5
8
|
const require_logging_ReactionLogger = require("../logging/ReactionLogger.cjs");
|
|
@@ -12,10 +15,25 @@ const require_EntityManager = require("../EntityManager.cjs");
|
|
|
12
15
|
*
|
|
13
16
|
* Source fields are usually regular/dumb primitives, i.e. `Author.age` is a source field
|
|
14
17
|
* of `Book.isPublic`, but derived fields can themselves be source fields as well.
|
|
18
|
+
* Guarded state transitions share this loop; their commit callbacks wait until SQL has been flushed.
|
|
15
19
|
*/
|
|
16
20
|
var ReactionsManager = class {
|
|
17
21
|
/** Stores all source `Reactables`s that have been marked for later traversal. */
|
|
18
22
|
pendingReactables = /* @__PURE__ */ new Map();
|
|
23
|
+
/** Queued state changes, including changes made by callbacks, are processed in reaction passes. */
|
|
24
|
+
pendingTransitions = [];
|
|
25
|
+
/**
|
|
26
|
+
* Initial assignments are collapsed until reactions first see the entity.
|
|
27
|
+
*
|
|
28
|
+
* Each entity keeps one creation step per transition field. Each step is also in `pendingTransitions`, and
|
|
29
|
+
* later assignments update its `to` in place. An entity usually has one or two transition fields, so a
|
|
30
|
+
* short array is cheaper than a nested Map.
|
|
31
|
+
*/
|
|
32
|
+
transitionCreations = /* @__PURE__ */ new Map();
|
|
33
|
+
/** New entities that reactions have seen, so their assignments are transitions. */
|
|
34
|
+
postCreationWindowEntities = /* @__PURE__ */ new Set();
|
|
35
|
+
/** Only matching callbacks are retained until SQL and final validation finish. */
|
|
36
|
+
pendingCommitTransitions = [];
|
|
19
37
|
/** Failed actions to retry post-hooks, deduped by target field name -> entity (rare error path). */
|
|
20
38
|
actionsPendingTypeErrors = /* @__PURE__ */ new Map();
|
|
21
39
|
/**
|
|
@@ -50,6 +68,76 @@ var ReactionsManager = class {
|
|
|
50
68
|
this.em = em;
|
|
51
69
|
}
|
|
52
70
|
/**
|
|
71
|
+
* Called by `setField` before changing an enum field, to check and queue the transition.
|
|
72
|
+
*
|
|
73
|
+
* An entity has no previous state until Joist's reactions have seen it, so every assignment to a new
|
|
74
|
+
* entity until then is part of its creation. I.e. `em.create` then `entity.status = Open`, or a factory
|
|
75
|
+
* setting a default and then a `withStatus` value, or `em.findOrCreate` creating then upserting, all
|
|
76
|
+
* create the entity as `Open`. Creation isn't checked against the `transitions` table.
|
|
77
|
+
*/
|
|
78
|
+
queueTransition(entity, fieldName, from, to) {
|
|
79
|
+
const meta = require_EntityMetadata.getMetadata(entity);
|
|
80
|
+
const transitions = meta.transitions.get(fieldName);
|
|
81
|
+
if (!transitions) return;
|
|
82
|
+
if (entity.isNewEntity && !this.postCreationWindowEntities.has(entity)) {
|
|
83
|
+
let creations = this.transitionCreations.get(entity);
|
|
84
|
+
if (!creations) this.transitionCreations.set(entity, creations = []);
|
|
85
|
+
const existing = creations.find((pending) => pending.fieldName === fieldName);
|
|
86
|
+
if (existing) existing.step.to = to;
|
|
87
|
+
else {
|
|
88
|
+
const pending = {
|
|
89
|
+
entity,
|
|
90
|
+
fieldName,
|
|
91
|
+
step: {
|
|
92
|
+
from: require_transitions.created,
|
|
93
|
+
to
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
creations.push(pending);
|
|
97
|
+
this.pendingTransitions.push(pending);
|
|
98
|
+
}
|
|
99
|
+
} else {
|
|
100
|
+
for (const table of transitions.tables) {
|
|
101
|
+
if (table.get(from)?.includes(to)) continue;
|
|
102
|
+
const message = `Cannot change ${fieldName} from ${require_transitions.describeValue(meta, fieldName, from)} to ${require_transitions.describeValue(meta, fieldName, to)}`;
|
|
103
|
+
throw new require_rules.ValidationErrors([{
|
|
104
|
+
entity,
|
|
105
|
+
message
|
|
106
|
+
}]);
|
|
107
|
+
}
|
|
108
|
+
this.pendingTransitions.push({
|
|
109
|
+
entity,
|
|
110
|
+
fieldName,
|
|
111
|
+
step: {
|
|
112
|
+
from,
|
|
113
|
+
to
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/** Forgets a field's creation transition after a factory assigns its trusted initial value. */
|
|
119
|
+
forgetCreationTransition(entity, fieldName) {
|
|
120
|
+
const creations = this.transitionCreations.get(entity);
|
|
121
|
+
const i = creations?.findIndex((pending) => pending.fieldName === fieldName) ?? -1;
|
|
122
|
+
if (i === -1) return;
|
|
123
|
+
const [pending] = creations.splice(i, 1);
|
|
124
|
+
this.pendingTransitions.splice(this.pendingTransitions.lastIndexOf(pending), 1);
|
|
125
|
+
}
|
|
126
|
+
/** Returns whether callbacks need a commit phase, even if there are no net SQL changes. */
|
|
127
|
+
hasPendingCommitTransitions() {
|
|
128
|
+
return this.pendingCommitTransitions.some((pending) => !pending.entity.isDeletedEntity);
|
|
129
|
+
}
|
|
130
|
+
/** Runs the queued callbacks after all SQL and validation succeed, without rechecking guards. */
|
|
131
|
+
async runCommitTransitions() {
|
|
132
|
+
const batch = this.pendingCommitTransitions.splice(0);
|
|
133
|
+
for (const wave of groupIntoEntityWaves(batch)) {
|
|
134
|
+
const results = await Promise.allSettled(wave.map(async (pending) => {
|
|
135
|
+
if (!pending.entity.isDeletedEntity) await pending.callback.run(pending.entity, this.em.ctx, pending.step);
|
|
136
|
+
}));
|
|
137
|
+
require_utils.failIfAnyRejected(results);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
53
141
|
* Queue all downstream reactive fields that depend on `fieldName` as a source field.
|
|
54
142
|
*
|
|
55
143
|
* This method can be synchronous b/c we internally queue the `Reactable` reverse
|
|
@@ -110,6 +198,7 @@ var ReactionsManager = class {
|
|
|
110
198
|
if (this.needsRecalc(kind)) this.logger.logStartingRecalc(this.em, kind);
|
|
111
199
|
let loops = 0;
|
|
112
200
|
while (this.needsRecalc(kind)) {
|
|
201
|
+
this.endTransitionCreations();
|
|
113
202
|
if (kind === "reactables") {
|
|
114
203
|
this.#needsRecalc.populate = false;
|
|
115
204
|
this.#needsRecalc.reaction = false;
|
|
@@ -172,6 +261,7 @@ var ReactionsManager = class {
|
|
|
172
261
|
});
|
|
173
262
|
}
|
|
174
263
|
if (failures.length > 0) throw failures[0];
|
|
264
|
+
if (kind === "reactables") await require_trusted.runInTrustedContext(() => this.processPendingTransitions());
|
|
175
265
|
for (const action of actions) if (action.r.runOnce) {
|
|
176
266
|
let set = this.processedActions.get(action.r.name);
|
|
177
267
|
if (!set) this.processedActions.set(action.r.name, set = /* @__PURE__ */ new Set());
|
|
@@ -200,10 +290,19 @@ var ReactionsManager = class {
|
|
|
200
290
|
get hasPendingTypeErrors() {
|
|
201
291
|
return this.actionsPendingTypeErrors.size > 0;
|
|
202
292
|
}
|
|
203
|
-
/** Clears
|
|
293
|
+
/** Clears pending reaction and transition work after a successful flush, including flushes with no SQL. */
|
|
204
294
|
clear() {
|
|
205
295
|
this.pendingReactables = /* @__PURE__ */ new Map();
|
|
206
296
|
this.processedActions.clear();
|
|
297
|
+
this.pendingTransitions.length = 0;
|
|
298
|
+
this.transitionCreations.clear();
|
|
299
|
+
this.postCreationWindowEntities.clear();
|
|
300
|
+
this.pendingCommitTransitions.length = 0;
|
|
301
|
+
this.#needsRecalc = {
|
|
302
|
+
populate: false,
|
|
303
|
+
query: false,
|
|
304
|
+
reaction: false
|
|
305
|
+
};
|
|
207
306
|
}
|
|
208
307
|
setLogger(logger) {
|
|
209
308
|
this.logger = logger ?? require_logging_ReactionLogger.noopReactionLogger;
|
|
@@ -225,7 +324,63 @@ var ReactionsManager = class {
|
|
|
225
324
|
byEntity.set(action.entity, action);
|
|
226
325
|
}
|
|
227
326
|
needsRecalc(kind) {
|
|
228
|
-
return kind === "reactables" ? this.#needsRecalc.populate || this.#needsRecalc.reaction : this.#needsRecalc.query;
|
|
327
|
+
return kind === "reactables" ? this.#needsRecalc.populate || this.#needsRecalc.reaction || this.pendingTransitions.length > 0 : this.#needsRecalc.query;
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Ends the creation of every new entity so far, before each loop of reactions.
|
|
331
|
+
*
|
|
332
|
+
* After this, assignments to those entities are transitions, i.e. an Approval created as `Requested`
|
|
333
|
+
* that a reaction then auto-approves records `Requested -> Approved`.
|
|
334
|
+
*/
|
|
335
|
+
endTransitionCreations() {
|
|
336
|
+
for (const entity of this.transitionCreations.keys()) this.postCreationWindowEntities.add(entity);
|
|
337
|
+
this.transitionCreations.clear();
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Evaluates each pending transition's guards once, then dispatches its matching callbacks.
|
|
341
|
+
*
|
|
342
|
+
* I.e. Pending -> Signed -> Pending restores an advance's original status, but the Signed guard must still run.
|
|
343
|
+
* Callbacks can append more transitions; taking one batch leaves those for the next reaction pass.
|
|
344
|
+
* Each wave processes one step per entity, so populate requests batch across entities without
|
|
345
|
+
* allowing an entity's next transition to overtake its previous callbacks.
|
|
346
|
+
*
|
|
347
|
+
* We might eventually treat A -> B -> A as an undo and ignore both transitions before reactions
|
|
348
|
+
* have seen them. For now, each transition is meaningful, even when the final state is unchanged.
|
|
349
|
+
*/
|
|
350
|
+
async processPendingTransitions() {
|
|
351
|
+
this.endTransitionCreations();
|
|
352
|
+
if (this.pendingTransitions.length === 0) return;
|
|
353
|
+
const batch = this.pendingTransitions.splice(0);
|
|
354
|
+
for (const wave of groupIntoEntityWaves(batch)) {
|
|
355
|
+
const guardResults = await Promise.allSettled(wave.map((pending) => this.evaluateTransitionGuards(pending)));
|
|
356
|
+
const errors = require_utils.failIfAnyRejected(guardResults).flat();
|
|
357
|
+
if (errors.length > 0) throw new require_rules.ValidationErrors(errors);
|
|
358
|
+
const callbackResults = await Promise.allSettled(wave.map((pending) => this.runTransitionCallbacks(pending)));
|
|
359
|
+
require_utils.failIfAnyRejected(callbackResults);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
/** Evaluates one transition's matching guards before any callbacks in its wave run. */
|
|
363
|
+
async evaluateTransitionGuards(pending) {
|
|
364
|
+
const { entity, fieldName, step } = pending;
|
|
365
|
+
if (entity.isDeletedEntity || step.from === require_transitions.created) return [];
|
|
366
|
+
const guards = require_EntityMetadata.getMetadata(entity).transitions.get(fieldName).guards.filter((guard) => require_transitions.matchesTransition(guard.matcher, step));
|
|
367
|
+
const results = await Promise.allSettled(guards.map((guard) => require_EntityManager.invokeRule(entity, () => guard.run(entity, step))));
|
|
368
|
+
return require_utils.failIfAnyRejected(results).flat();
|
|
369
|
+
}
|
|
370
|
+
/** Runs one entity's callbacks in registration order after all guards in the wave have passed. */
|
|
371
|
+
async runTransitionCallbacks(pending) {
|
|
372
|
+
const { entity, fieldName, step } = pending;
|
|
373
|
+
if (entity.isDeletedEntity) return;
|
|
374
|
+
for (const callback of require_EntityMetadata.getMetadata(entity).transitions.get(fieldName).callbacks) {
|
|
375
|
+
if (entity.isDeletedEntity) break;
|
|
376
|
+
if (!require_transitions.matchesTransition(callback.matcher, step)) continue;
|
|
377
|
+
if (callback.phase === "commit") this.pendingCommitTransitions.push({
|
|
378
|
+
entity,
|
|
379
|
+
callback,
|
|
380
|
+
step
|
|
381
|
+
});
|
|
382
|
+
else await callback.run(entity, this.em.ctx, step);
|
|
383
|
+
}
|
|
229
384
|
}
|
|
230
385
|
getPending(r) {
|
|
231
386
|
let pending = this.pendingReactables.get(r);
|
|
@@ -256,6 +411,17 @@ var ReactionsManager = class {
|
|
|
256
411
|
return (require_EntityManager.getEmInternalApi(this.em).isMerging(entity) ? meta.reactablesIncludingReadOnlyByField : meta.reactablesByField).get(fieldName) ?? noReactables;
|
|
257
412
|
}
|
|
258
413
|
};
|
|
414
|
+
/** Groups queued work into parallel waves while preserving each entity's input order. */
|
|
415
|
+
function groupIntoEntityWaves(batch) {
|
|
416
|
+
const waves = [];
|
|
417
|
+
const waveByEntity = /* @__PURE__ */ new Map();
|
|
418
|
+
for (const pending of batch) {
|
|
419
|
+
const wave = waveByEntity.get(pending.entity) ?? 0;
|
|
420
|
+
(waves[wave] ??= []).push(pending);
|
|
421
|
+
waveByEntity.set(pending.entity, wave + 1);
|
|
422
|
+
}
|
|
423
|
+
return waves;
|
|
424
|
+
}
|
|
259
425
|
/** A shared frozen array for fields with no downstream reactables. */
|
|
260
426
|
const noReactables = Object.freeze([]);
|
|
261
427
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ReactionsManager.cjs","names":["globalLogger","noopReactionLogger","#needsRecalc","getMetadata","followReverseHint","runInTrustedContext","#doAction","NoIdError","getEmInternalApi"],"sources":["../../src/reactivity/ReactionsManager.ts"],"sourcesContent":["import type { Reactable } from \"src/config.ts\";\nimport type { Entity } from \"src/Entity.ts\";\nimport { type EntityManager, getEmInternalApi } from \"src/EntityManager.ts\";\nimport { type EntityMetadata, getMetadata } from \"src/EntityMetadata.ts\";\nimport { NoIdError } from \"src/index.ts\";\nimport { type ReactionLogger, globalLogger, noopReactionLogger } from \"src/logging/ReactionLogger.ts\";\nimport { followReverseHint } from \"src/reactivity/reactiveHints.ts\";\nimport { runInTrustedContext } from \"src/trusted.ts\";\n\nexport type ReactiveAction = { r: Reactable; entity: Entity };\n/**\n * Manages the reactivity of tracking which source fields have changed and finding/recalculating\n * their downstream derived fields.\n *\n * Source fields are usually regular/dumb primitives, i.e. `Author.age` is a source field\n * of `Book.isPublic`, but derived fields can themselves be source fields as well.\n */\nexport class ReactionsManager {\n /** Stores all source `Reactables`s that have been marked for later traversal. */\n private pendingReactables: Map<Reactable, { todo: Set<Entity>; done: Set<Entity> }> = new Map();\n /** Failed actions to retry post-hooks, deduped by target field name -> entity (rare error path). */\n private actionsPendingTypeErrors: Map<string, Map<Entity, ReactiveAction>> = new Map();\n /**\n * A map of entity tagName -> fields that have been marked as dirty.\n *\n * The key of the map is \"just\" the tag name, instead of a specific entity, b/c at the time of\n * being marked dirty, we only have the upstream/source entity + fieldName that has changed, and\n * not the 1-or-more downstream/target entities that will actually recalc.\n *\n * Instead, we just track the dirty fields by type-of-entity, which is enough for `isPendingRecalc`.\n */\n private dirtyFields: Map<string, Set<string>> = new Map();\n /**\n * Tracks which entities have already run a `runOnce` reactable, keyed by the target field\n * name -> entities. We key on `r.name` (not `r` itself) b/c two `Reactable`s reverse-indexed\n * from different source paths can target the same derived field, and must dedupe together.\n * Keying the outer map by field name (of which there are few) instead of by entity (of which\n * there are many) means we allocate just one `Set` per distinct field name, rather than a\n * `Set` per entity or the old per-action `${entity.toTaggedString()}_${r.name}` key strings.\n */\n private processedActions: Map<string, Set<Entity>> = new Map();\n #needsRecalc = { populate: false, query: false, reaction: false };\n // Accessible for EntityManager.runValidation to reuse\n logger: ReactionLogger = globalLogger ?? noopReactionLogger;\n private em: EntityManager;\n /** These are NPEs that *might* have been from invalid m2o fields, so we only throw them after validation. */\n private suppressedTypeErrors: Error[] = [];\n\n constructor(em: EntityManager) {\n this.em = em;\n }\n\n /**\n * Queue all downstream reactive fields that depend on `fieldName` as a source field.\n *\n * This method can be synchronous b/c we internally queue the `Reactable` reverse\n * indexes on the given source `fieldName`, and don't crawl/walk back to the downstream\n * entities/fields until `recalcPendingReactables` is called.\n */\n queueDownstreamReactables(entity: Entity, fieldName: string): void {\n // Use the reverse index of Reactables that configureMetadata sets up\n for (const r of this.getReactablesByField(entity, fieldName)) {\n // We always queue the reactable/entity, even if we're mid-flush or even mid-recalc, to avoid:\n // - firstName is changed from a1 to a2\n // - this triggers firstName's reactable to `Author.initials` to be queued\n // - during the 1st em.flush loop, we recalc `Author.initials` and it didn't change\n // - during the 1st em.flush loop, a hook changes firstName from a2 to b2\n // - if we skip re-queuing firstName's reactables, we will miss that initials needs\n // its `.load()` called again so that it's `setField` marks `initials` as\n // dirty, otherwise it will be left out of any INSERTs/UPDATEs.\n this.getPending(r).todo.add(entity);\n this.#needsRecalc[r.kind] = true;\n this.logger.logQueued(entity, fieldName, r);\n }\n }\n\n /** Dequeues reactivity on `fieldName`, i.e. if it's no longer dirty. */\n dequeueDownstreamReactables(entity: Entity, fieldName: string): void {\n // Use the reverse index of Reactables that configureMetadata sets up\n for (const r of this.getReactablesByField(entity, fieldName)) {\n const pending = this.getPending(r);\n if (pending.done.has(entity)) {\n // Ironically, if we've already run this reactable, asking to dequeue probably means\n // we need to run it again (to recalc its value), b/c this could be a mid-flush change, i.e.:\n // - firstName = a1 from db\n // - firstName is changed to a2, triggers initials reactable\n // - firstName is changed back to a1, which is the original value, so setField\n // thinks we can dequeue the reactable\n // - but actually our reactable needs to be re-run with the restored value\n pending.todo.add(entity);\n this.#needsRecalc[r.kind] = true;\n } else if (r.fields.length === 1) {\n // We can only delete/dequeue a reaction if `fieldName` is the only or last field\n // that had triggered `r` to run. Since we don't track that currently, i.e. we'd\n // need to have a `Pending.dirtyFields`, for now just only dequeue if `r` only\n // has one field (which is us) anyway.\n pending.todo.delete(entity);\n }\n }\n }\n\n /** Queue all downstream reactive fields that depend on this entity being created or deleted. */\n queueAllDownstreamFields(entity: Entity, reason: \"created\" | \"deleted\"): void {\n for (const r of this.getReactables(entity)) {\n this.getPending(r).todo.add(entity);\n this.#needsRecalc[r.kind] = true;\n this.logger.logQueuedAll(entity, reason, r);\n }\n }\n\n /**\n * Returns whether this field might be pending recalc.\n *\n * This is technically a guess, b/c our reaction infra may not yet have crawled from an upstream\n * source field down to the `N` specific target entities that need recalced, and instead just\n * knows \"it will need to do that soon\" i.e. at the next `em.flush`.\n *\n * So, instead this is a heuristic that reports if `fieldName` has been marked dirty for _some_\n * entities of type `entity`, but we don't technically know if that's _this_ entity.\n *\n * I.e. this might return false positives, but should never return false negatives.\n */\n isMaybePendingRecalc(entity: Entity, fieldName: string): boolean {\n return this.getDirtyFields(getMetadata(entity)).has(fieldName);\n }\n\n hasPendingReactiveQueries(): boolean {\n return this.#needsRecalc.query;\n }\n\n /**\n * Given source `Reactable` \"reverse indexes\" that have been queued as dirty by calls\n * to setters, `em.register`, or `em.delete`, asynchronously walks/crawls to the downstream\n * derived fields and calls `.load()` on them to calc.\n *\n * We also do this in a loop to handle reactive fields depending on other reactive fields.\n */\n async recalcPendingReactables(kind: \"reactables\" | \"reactiveQueries\") {\n if (this.needsRecalc(kind)) this.logger.logStartingRecalc(this.em, kind);\n\n let loops = 0;\n while (this.needsRecalc(kind)) {\n // ...we probably should only loop for `kind=reactables` Reactables, and `kind=reactiveQueries`\n // AsyncReactiveFields should probably only have a single loop, after which we return and\n // let `em.flush` push the latest values to the db, so our 2nd-order AsyncReactiveFields\n // will see the latest value.\n if (kind === \"reactables\") {\n this.#needsRecalc.populate = false;\n this.#needsRecalc.reaction = false;\n } else {\n this.#needsRecalc.query = false;\n }\n\n // Dedupe actions by (target field name, entity); see the `processedActions` docs for why\n // we key on `r.name` instead of `r` or the old per-action key strings\n const seen: Map<string, Set<Entity>> = new Map();\n const actions: ReactiveAction[] = [];\n\n const walks = await Promise.all(\n [...this.pendingReactables.entries()].map(async ([r, pending]) => {\n // Skip reactive queries until post-flush\n if (kind === \"reactables\" && r.kind === \"query\") return undefined;\n if (kind === \"reactiveQueries\" && (r.kind === \"populate\" || r.kind === \"reaction\")) return undefined;\n // Copy pending and clear it\n const todo = [...pending.todo];\n if (todo.length === 0) return undefined;\n pending.todo.clear();\n for (const doing of todo) pending.done.add(doing);\n // Walk back from the source to any downstream entities\n const entities = r.path.length === 0 ? todo : await followReverseHint(r.name, todo, r.path);\n const actionableEntities = entities.filter((entity) => !entity.isDeletedEntity && entity instanceof r.cstr);\n let seenEntities = seen.get(r.name);\n if (!seenEntities) seen.set(r.name, (seenEntities = new Set()));\n const processed = r.runOnce ? this.processedActions.get(r.name) : undefined;\n for (const entity of actionableEntities) {\n // We could arrive at the same reactable from multiple paths (eg, 2 dependent fields changed), so we need to\n // dedupe based on the entity and reactable to only run each action once for any given entity per loop\n if (seenEntities.has(entity)) continue;\n // If this reactable has already run and shouldn't run again, then skip it\n if (processed && processed.has(entity)) continue;\n seenEntities.add(entity);\n actions.push({ r, entity });\n }\n return { r, todo, entities: actionableEntities };\n }),\n );\n this.logger.logWalks(walks, \"recalc\");\n this.logger.logLoadingStart(this.em, actions);\n // Use allSettled so that we can watch for derived values that want to use an entity's id\n // i.e. they can fail, but we'll queue them from later.\n const startTime = this.logger.now();\n const results = await runInTrustedContext(() => Promise.allSettled(actions.map((a) => this.#doAction(a))));\n const endTime = this.logger.now();\n this.logger.logLoadingEnd(this.em, endTime - startTime);\n\n // This loop's actions are already unique by (name, entity), so a plain array dedupes fine\n const actionsPendingAssignedIds: ReactiveAction[] = [];\n const failures: any[] = [];\n results.forEach((result, i) => {\n if (result.status === \"rejected\") {\n // Let `author.id` and `book.author.get.firstName` errors run again after flush/hooks fills them in\n if (result.reason instanceof NoIdError) {\n actionsPendingAssignedIds.push(actions[i]);\n } else if (result.reason instanceof TypeError) {\n this.addPendingTypeError(actions[i]);\n } else {\n failures.push(result.reason);\n }\n }\n });\n\n // If we have any actions that need to be re-run because they failed due to a missing id, then we assign ids and\n // re-run them.\n if (actionsPendingAssignedIds.length > 0) {\n await this.em.assignNewIds();\n this.logger.logLoadingStart(this.em, actionsPendingAssignedIds);\n const startTime = this.logger.now();\n const results = await runInTrustedContext(() =>\n Promise.allSettled(actionsPendingAssignedIds.map((a) => this.#doAction(a))),\n );\n const endTime = this.logger.now();\n this.logger.logLoadingEnd(this.em, endTime - startTime);\n results.forEach((result, i) => {\n if (result.status === \"rejected\") {\n if (result.reason instanceof TypeError) {\n this.addPendingTypeError(actionsPendingAssignedIds[i]);\n } else {\n failures.push(result.reason);\n }\n }\n });\n }\n\n if (failures.length > 0) throw failures[0];\n // Record any successful actions that should only run once so we don't run them again\n for (const action of actions) {\n if (action.r.runOnce) {\n let set = this.processedActions.get(action.r.name);\n if (!set) this.processedActions.set(action.r.name, (set = new Set()));\n set.add(action.entity);\n }\n }\n // This should generally not happen, only if two reactive fields depend on each other,\n // which in theory should probably be caught/blow up in the `configureMetadata` step,\n // but if it's not caught sooner, at least don't infinite loop.\n if (loops++ > 50) {\n throw new Error(\"recalc looped too many times, probably a circular dependency\");\n }\n }\n }\n\n async recalcPendingTypeErrors() {\n const actions: ReactiveAction[] = [];\n for (const byEntity of this.actionsPendingTypeErrors.values()) {\n for (const action of byEntity.values()) actions.push(action);\n }\n this.actionsPendingTypeErrors.clear();\n\n const startTime = this.logger.now();\n const results = await runInTrustedContext(() =>\n Promise.allSettled(actions.filter((a) => !a.entity.isDeletedEntity).map((a) => this.#doAction(a))),\n );\n const endTime = this.logger.now();\n this.logger.logLoadingEnd(this.em, endTime - startTime);\n\n const failures: any[] = [];\n results.forEach((result) => {\n if (result.status === \"rejected\") {\n if (result.reason instanceof TypeError) {\n // Defer to the validation error to catch this\n this.suppressedTypeErrors.push(result.reason);\n } else {\n failures.push(result.reason);\n }\n }\n });\n if (failures.length > 0) throw failures[0];\n }\n\n get hasPendingTypeErrors(): boolean {\n return this.actionsPendingTypeErrors.size > 0;\n }\n\n /** Clears all the pending source fields, i.e. after `em.flush` is complete. */\n clear(): void {\n this.pendingReactables = new Map();\n this.processedActions.clear();\n }\n\n setLogger(logger: ReactionLogger | undefined): void {\n this.logger = logger ?? noopReactionLogger;\n }\n\n clearSuppressedTypeErrors(): void {\n this.suppressedTypeErrors = [];\n }\n\n throwIfAnySuppressedTypeErrors(): void {\n if (this.suppressedTypeErrors.length > 0) {\n throw this.suppressedTypeErrors[0];\n }\n }\n\n #doAction(action: ReactiveAction) {\n const { r, entity } = action;\n return r.kind === \"reaction\" ? r.fn(entity, this.em.ctx) : (entity as any)[r.name].load();\n }\n\n /** Remembers a TypeError'd action to retry post-hooks, deduped by (target field name, entity). */\n private addPendingTypeError(action: ReactiveAction): void {\n let byEntity = this.actionsPendingTypeErrors.get(action.r.name);\n if (!byEntity) this.actionsPendingTypeErrors.set(action.r.name, (byEntity = new Map()));\n byEntity.set(action.entity, action);\n }\n\n needsRecalc(kind: \"reactables\" | \"reactiveQueries\"): boolean {\n return kind === \"reactables\" ? this.#needsRecalc.populate || this.#needsRecalc.reaction : this.#needsRecalc.query;\n }\n\n private getPending(r: Reactable): { todo: Set<Entity>; done: Set<Entity> } {\n let pending = this.pendingReactables.get(r);\n if (!pending) {\n pending = { todo: new Set(), done: new Set() };\n this.pendingReactables.set(r, pending);\n // The dirty field name is constant per reactable, so mark it once on the first\n // queue instead of paying the `getMetadata` + map lookup on every queued entity.\n this.getDirtyFields(getMetadata(r.cstr)).add(r.name);\n }\n return pending;\n }\n\n private getDirtyFields(meta: EntityMetadata): Set<string> {\n let dirty = this.dirtyFields.get(meta.tagName);\n if (!dirty) {\n dirty = new Set();\n this.dirtyFields.set(meta.tagName, dirty);\n }\n return dirty;\n }\n\n private getReactables(entity: Entity): Reactable[] {\n // If two books are getting merged, and so a normally-immutable `BookReview.book` is being changed,\n // then even normally-immutable fields need to be recalculated.\n const meta = getMetadata(entity);\n return getEmInternalApi(this.em).isMerging(entity) ? meta.reactablesIncludingReadOnly! : meta.reactables!;\n }\n\n private getReactablesByField(entity: Entity, fieldName: string): readonly Reactable[] {\n const meta = getMetadata(entity);\n const byField = getEmInternalApi(this.em).isMerging(entity)\n ? meta.reactablesIncludingReadOnlyByField!\n : meta.reactablesByField!;\n // Most fields have no reactables, so return a shared empty array instead of allocating one per set\n return byField.get(fieldName) ?? noReactables;\n }\n}\n\n/** A shared frozen array for fields with no downstream reactables. */\nconst noReactables: readonly Reactable[] = Object.freeze([]);\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ReactionsManager.cjs","names":["globalLogger","noopReactionLogger","getMetadata","created","describeValue","ValidationErrors","#needsRecalc","followReverseHint","runInTrustedContext","#doAction","NoIdError","failIfAnyRejected","matchesTransition","invokeRule","getEmInternalApi"],"sources":["../../src/reactivity/ReactionsManager.ts"],"sourcesContent":["import type { Reactable } from \"src/config.ts\";\nimport type { Entity } from \"src/Entity.ts\";\nimport { type EntityManager, getEmInternalApi, invokeRule } from \"src/EntityManager.ts\";\nimport { type EntityMetadata, getMetadata } from \"src/EntityMetadata.ts\";\nimport { NoIdError } from \"src/index.ts\";\nimport { type ReactionLogger, globalLogger, noopReactionLogger } from \"src/logging/ReactionLogger.ts\";\nimport { followReverseHint } from \"src/reactivity/reactiveHints.ts\";\nimport { type ValidationError, ValidationErrors } from \"src/rules.ts\";\nimport {\n type TransitionCallback,\n type TransitionStep,\n created,\n describeValue,\n matchesTransition,\n} from \"src/transitions.ts\";\nimport { runInTrustedContext } from \"src/trusted.ts\";\nimport { failIfAnyRejected } from \"src/utils.ts\";\n\nexport type ReactiveAction = { r: Reactable; entity: Entity };\n/**\n * Manages the reactivity of tracking which source fields have changed and finding/recalculating\n * their downstream derived fields.\n *\n * Source fields are usually regular/dumb primitives, i.e. `Author.age` is a source field\n * of `Book.isPublic`, but derived fields can themselves be source fields as well.\n * Guarded state transitions share this loop; their commit callbacks wait until SQL has been flushed.\n */\nexport class ReactionsManager {\n /** Stores all source `Reactables`s that have been marked for later traversal. */\n private pendingReactables: Map<Reactable, { todo: Set<Entity>; done: Set<Entity> }> = new Map();\n /** Queued state changes, including changes made by callbacks, are processed in reaction passes. */\n private pendingTransitions: PendingTransition[] = [];\n /**\n * Initial assignments are collapsed until reactions first see the entity.\n *\n * Each entity keeps one creation step per transition field. Each step is also in `pendingTransitions`, and\n * later assignments update its `to` in place. An entity usually has one or two transition fields, so a\n * short array is cheaper than a nested Map.\n */\n private transitionCreations: Map<Entity, PendingTransition[]> = new Map();\n /** New entities that reactions have seen, so their assignments are transitions. */\n private postCreationWindowEntities: Set<Entity> = new Set();\n /** Only matching callbacks are retained until SQL and final validation finish. */\n private pendingCommitTransitions: PendingCommitCallback[] = [];\n /** Failed actions to retry post-hooks, deduped by target field name -> entity (rare error path). */\n private actionsPendingTypeErrors: Map<string, Map<Entity, ReactiveAction>> = new Map();\n /**\n * A map of entity tagName -> fields that have been marked as dirty.\n *\n * The key of the map is \"just\" the tag name, instead of a specific entity, b/c at the time of\n * being marked dirty, we only have the upstream/source entity + fieldName that has changed, and\n * not the 1-or-more downstream/target entities that will actually recalc.\n *\n * Instead, we just track the dirty fields by type-of-entity, which is enough for `isPendingRecalc`.\n */\n private dirtyFields: Map<string, Set<string>> = new Map();\n /**\n * Tracks which entities have already run a `runOnce` reactable, keyed by the target field\n * name -> entities. We key on `r.name` (not `r` itself) b/c two `Reactable`s reverse-indexed\n * from different source paths can target the same derived field, and must dedupe together.\n * Keying the outer map by field name (of which there are few) instead of by entity (of which\n * there are many) means we allocate just one `Set` per distinct field name, rather than a\n * `Set` per entity or the old per-action `${entity.toTaggedString()}_${r.name}` key strings.\n */\n private processedActions: Map<string, Set<Entity>> = new Map();\n #needsRecalc = { populate: false, query: false, reaction: false };\n // Accessible for EntityManager.runValidation to reuse\n logger: ReactionLogger = globalLogger ?? noopReactionLogger;\n private em: EntityManager;\n /** These are NPEs that *might* have been from invalid m2o fields, so we only throw them after validation. */\n private suppressedTypeErrors: Error[] = [];\n\n constructor(em: EntityManager) {\n this.em = em;\n }\n\n /**\n * Called by `setField` before changing an enum field, to check and queue the transition.\n *\n * An entity has no previous state until Joist's reactions have seen it, so every assignment to a new\n * entity until then is part of its creation. I.e. `em.create` then `entity.status = Open`, or a factory\n * setting a default and then a `withStatus` value, or `em.findOrCreate` creating then upserting, all\n * create the entity as `Open`. Creation isn't checked against the `transitions` table.\n */\n queueTransition(entity: Entity, fieldName: string, from: unknown, to: unknown): void {\n const meta = getMetadata(entity);\n const transitions = meta.transitions!.get(fieldName);\n if (!transitions) return;\n // If this is a new entity still within its creation window, only keep the last step\n if (entity.isNewEntity && !this.postCreationWindowEntities.has(entity)) {\n let creations = this.transitionCreations.get(entity);\n if (!creations) this.transitionCreations.set(entity, (creations = []));\n const existing = creations.find((pending) => pending.fieldName === fieldName);\n if (existing) {\n existing.step.to = to;\n } else {\n const pending = { entity, fieldName, step: { from: created, to } };\n creations.push(pending);\n this.pendingTransitions.push(pending);\n }\n } else {\n // Base and subtype tables are all checked, so each type can only restrict its base type's changes\n for (const table of transitions.tables) {\n if (table.get(from)?.includes(to)) continue;\n const message = `Cannot change ${fieldName} from ${describeValue(meta, fieldName, from)} to ${describeValue(meta, fieldName, to)}`;\n throw new ValidationErrors([{ entity, message }]);\n }\n this.pendingTransitions.push({ entity, fieldName, step: { from, to } });\n }\n }\n\n /** Forgets a field's creation transition after a factory assigns its trusted initial value. */\n forgetCreationTransition(entity: Entity, fieldName: string): void {\n const creations = this.transitionCreations.get(entity);\n const i = creations?.findIndex((pending) => pending.fieldName === fieldName) ?? -1;\n if (i === -1) return;\n // Remove it from both lists, so `needsRecalc` doesn't run an empty reaction pass for it\n const [pending] = creations!.splice(i, 1);\n this.pendingTransitions.splice(this.pendingTransitions.lastIndexOf(pending), 1);\n }\n\n /** Returns whether callbacks need a commit phase, even if there are no net SQL changes. */\n hasPendingCommitTransitions(): boolean {\n return this.pendingCommitTransitions.some((pending) => !pending.entity.isDeletedEntity);\n }\n\n /** Runs the queued callbacks after all SQL and validation succeed, without rechecking guards. */\n async runCommitTransitions(): Promise<void> {\n const batch = this.pendingCommitTransitions.splice(0);\n for (const wave of groupIntoEntityWaves(batch)) {\n const results = await Promise.allSettled(\n wave.map(async (pending) => {\n if (!pending.entity.isDeletedEntity) {\n await pending.callback.run(pending.entity, this.em.ctx, pending.step);\n }\n }),\n );\n failIfAnyRejected(results);\n }\n }\n\n /**\n * Queue all downstream reactive fields that depend on `fieldName` as a source field.\n *\n * This method can be synchronous b/c we internally queue the `Reactable` reverse\n * indexes on the given source `fieldName`, and don't crawl/walk back to the downstream\n * entities/fields until `recalcPendingReactables` is called.\n */\n queueDownstreamReactables(entity: Entity, fieldName: string): void {\n // Use the reverse index of Reactables that configureMetadata sets up\n for (const r of this.getReactablesByField(entity, fieldName)) {\n // We always queue the reactable/entity, even if we're mid-flush or even mid-recalc, to avoid:\n // - firstName is changed from a1 to a2\n // - this triggers firstName's reactable to `Author.initials` to be queued\n // - during the 1st em.flush loop, we recalc `Author.initials` and it didn't change\n // - during the 1st em.flush loop, a hook changes firstName from a2 to b2\n // - if we skip re-queuing firstName's reactables, we will miss that initials needs\n // its `.load()` called again so that it's `setField` marks `initials` as\n // dirty, otherwise it will be left out of any INSERTs/UPDATEs.\n this.getPending(r).todo.add(entity);\n this.#needsRecalc[r.kind] = true;\n this.logger.logQueued(entity, fieldName, r);\n }\n }\n\n /** Dequeues reactivity on `fieldName`, i.e. if it's no longer dirty. */\n dequeueDownstreamReactables(entity: Entity, fieldName: string): void {\n // Use the reverse index of Reactables that configureMetadata sets up\n for (const r of this.getReactablesByField(entity, fieldName)) {\n const pending = this.getPending(r);\n if (pending.done.has(entity)) {\n // Ironically, if we've already run this reactable, asking to dequeue probably means\n // we need to run it again (to recalc its value), b/c this could be a mid-flush change, i.e.:\n // - firstName = a1 from db\n // - firstName is changed to a2, triggers initials reactable\n // - firstName is changed back to a1, which is the original value, so setField\n // thinks we can dequeue the reactable\n // - but actually our reactable needs to be re-run with the restored value\n pending.todo.add(entity);\n this.#needsRecalc[r.kind] = true;\n } else if (r.fields.length === 1) {\n // We can only delete/dequeue a reaction if `fieldName` is the only or last field\n // that had triggered `r` to run. Since we don't track that currently, i.e. we'd\n // need to have a `Pending.dirtyFields`, for now just only dequeue if `r` only\n // has one field (which is us) anyway.\n pending.todo.delete(entity);\n }\n }\n }\n\n /** Queue all downstream reactive fields that depend on this entity being created or deleted. */\n queueAllDownstreamFields(entity: Entity, reason: \"created\" | \"deleted\"): void {\n for (const r of this.getReactables(entity)) {\n this.getPending(r).todo.add(entity);\n this.#needsRecalc[r.kind] = true;\n this.logger.logQueuedAll(entity, reason, r);\n }\n }\n\n /**\n * Returns whether this field might be pending recalc.\n *\n * This is technically a guess, b/c our reaction infra may not yet have crawled from an upstream\n * source field down to the `N` specific target entities that need recalced, and instead just\n * knows \"it will need to do that soon\" i.e. at the next `em.flush`.\n *\n * So, instead this is a heuristic that reports if `fieldName` has been marked dirty for _some_\n * entities of type `entity`, but we don't technically know if that's _this_ entity.\n *\n * I.e. this might return false positives, but should never return false negatives.\n */\n isMaybePendingRecalc(entity: Entity, fieldName: string): boolean {\n return this.getDirtyFields(getMetadata(entity)).has(fieldName);\n }\n\n hasPendingReactiveQueries(): boolean {\n return this.#needsRecalc.query;\n }\n\n /**\n * Given source `Reactable` \"reverse indexes\" that have been queued as dirty by calls\n * to setters, `em.register`, or `em.delete`, asynchronously walks/crawls to the downstream\n * derived fields and calls `.load()` on them to calc.\n *\n * We also do this in a loop to handle reactive fields depending on other reactive fields.\n */\n async recalcPendingReactables(kind: \"reactables\" | \"reactiveQueries\") {\n if (this.needsRecalc(kind)) this.logger.logStartingRecalc(this.em, kind);\n\n let loops = 0;\n while (this.needsRecalc(kind)) {\n // Reactions are about to see every new entity so far, so later assignments to them are transitions\n this.endTransitionCreations();\n // ...we probably should only loop for `kind=reactables` Reactables, and `kind=reactiveQueries`\n // AsyncReactiveFields should probably only have a single loop, after which we return and\n // let `em.flush` push the latest values to the db, so our 2nd-order AsyncReactiveFields\n // will see the latest value.\n if (kind === \"reactables\") {\n this.#needsRecalc.populate = false;\n this.#needsRecalc.reaction = false;\n } else {\n this.#needsRecalc.query = false;\n }\n\n // Dedupe actions by (target field name, entity); see the `processedActions` docs for why\n // we key on `r.name` instead of `r` or the old per-action key strings\n const seen: Map<string, Set<Entity>> = new Map();\n const actions: ReactiveAction[] = [];\n\n const walks = await Promise.all(\n [...this.pendingReactables.entries()].map(async ([r, pending]) => {\n // Skip reactive queries until post-flush\n if (kind === \"reactables\" && r.kind === \"query\") return undefined;\n if (kind === \"reactiveQueries\" && (r.kind === \"populate\" || r.kind === \"reaction\")) return undefined;\n // Copy pending and clear it\n const todo = [...pending.todo];\n if (todo.length === 0) return undefined;\n pending.todo.clear();\n for (const doing of todo) pending.done.add(doing);\n // Walk back from the source to any downstream entities\n const entities = r.path.length === 0 ? todo : await followReverseHint(r.name, todo, r.path);\n const actionableEntities = entities.filter((entity) => !entity.isDeletedEntity && entity instanceof r.cstr);\n let seenEntities = seen.get(r.name);\n if (!seenEntities) seen.set(r.name, (seenEntities = new Set()));\n const processed = r.runOnce ? this.processedActions.get(r.name) : undefined;\n for (const entity of actionableEntities) {\n // We could arrive at the same reactable from multiple paths (eg, 2 dependent fields changed), so we need to\n // dedupe based on the entity and reactable to only run each action once for any given entity per loop\n if (seenEntities.has(entity)) continue;\n // If this reactable has already run and shouldn't run again, then skip it\n if (processed && processed.has(entity)) continue;\n seenEntities.add(entity);\n actions.push({ r, entity });\n }\n return { r, todo, entities: actionableEntities };\n }),\n );\n this.logger.logWalks(walks, \"recalc\");\n this.logger.logLoadingStart(this.em, actions);\n // Use allSettled so that we can watch for derived values that want to use an entity's id\n // i.e. they can fail, but we'll queue them from later.\n const startTime = this.logger.now();\n const results = await runInTrustedContext(() => Promise.allSettled(actions.map((a) => this.#doAction(a))));\n const endTime = this.logger.now();\n this.logger.logLoadingEnd(this.em, endTime - startTime);\n\n // This loop's actions are already unique by (name, entity), so a plain array dedupes fine\n const actionsPendingAssignedIds: ReactiveAction[] = [];\n const failures: any[] = [];\n results.forEach((result, i) => {\n if (result.status === \"rejected\") {\n // Let `author.id` and `book.author.get.firstName` errors run again after flush/hooks fills them in\n if (result.reason instanceof NoIdError) {\n actionsPendingAssignedIds.push(actions[i]);\n } else if (result.reason instanceof TypeError) {\n this.addPendingTypeError(actions[i]);\n } else {\n failures.push(result.reason);\n }\n }\n });\n\n // If we have any actions that need to be re-run because they failed due to a missing id, then we assign ids and\n // re-run them.\n if (actionsPendingAssignedIds.length > 0) {\n await this.em.assignNewIds();\n this.logger.logLoadingStart(this.em, actionsPendingAssignedIds);\n const startTime = this.logger.now();\n const results = await runInTrustedContext(() =>\n Promise.allSettled(actionsPendingAssignedIds.map((a) => this.#doAction(a))),\n );\n const endTime = this.logger.now();\n this.logger.logLoadingEnd(this.em, endTime - startTime);\n results.forEach((result, i) => {\n if (result.status === \"rejected\") {\n if (result.reason instanceof TypeError) {\n this.addPendingTypeError(actionsPendingAssignedIds[i]);\n } else {\n failures.push(result.reason);\n }\n }\n });\n }\n\n if (failures.length > 0) throw failures[0];\n if (kind === \"reactables\") {\n await runInTrustedContext(() => this.processPendingTransitions());\n }\n // Record any successful actions that should only run once so we don't run them again\n for (const action of actions) {\n if (action.r.runOnce) {\n let set = this.processedActions.get(action.r.name);\n if (!set) this.processedActions.set(action.r.name, (set = new Set()));\n set.add(action.entity);\n }\n }\n // This should generally not happen, only if two reactive fields depend on each other,\n // which in theory should probably be caught/blow up in the `configureMetadata` step,\n // but if it's not caught sooner, at least don't infinite loop.\n if (loops++ > 50) {\n throw new Error(\"recalc looped too many times, probably a circular dependency\");\n }\n }\n }\n\n async recalcPendingTypeErrors() {\n const actions: ReactiveAction[] = [];\n for (const byEntity of this.actionsPendingTypeErrors.values()) {\n for (const action of byEntity.values()) actions.push(action);\n }\n this.actionsPendingTypeErrors.clear();\n\n const startTime = this.logger.now();\n const results = await runInTrustedContext(() =>\n Promise.allSettled(actions.filter((a) => !a.entity.isDeletedEntity).map((a) => this.#doAction(a))),\n );\n const endTime = this.logger.now();\n this.logger.logLoadingEnd(this.em, endTime - startTime);\n\n const failures: any[] = [];\n results.forEach((result) => {\n if (result.status === \"rejected\") {\n if (result.reason instanceof TypeError) {\n // Defer to the validation error to catch this\n this.suppressedTypeErrors.push(result.reason);\n } else {\n failures.push(result.reason);\n }\n }\n });\n if (failures.length > 0) throw failures[0];\n }\n\n get hasPendingTypeErrors(): boolean {\n return this.actionsPendingTypeErrors.size > 0;\n }\n\n /** Clears pending reaction and transition work after a successful flush, including flushes with no SQL. */\n clear(): void {\n this.pendingReactables = new Map();\n this.processedActions.clear();\n this.pendingTransitions.length = 0;\n this.transitionCreations.clear();\n this.postCreationWindowEntities.clear();\n this.pendingCommitTransitions.length = 0;\n this.#needsRecalc = { populate: false, query: false, reaction: false };\n }\n\n setLogger(logger: ReactionLogger | undefined): void {\n this.logger = logger ?? noopReactionLogger;\n }\n\n clearSuppressedTypeErrors(): void {\n this.suppressedTypeErrors = [];\n }\n\n throwIfAnySuppressedTypeErrors(): void {\n if (this.suppressedTypeErrors.length > 0) {\n throw this.suppressedTypeErrors[0];\n }\n }\n\n #doAction(action: ReactiveAction) {\n const { r, entity } = action;\n return r.kind === \"reaction\" ? r.fn(entity, this.em.ctx) : (entity as any)[r.name].load();\n }\n\n /** Remembers a TypeError'd action to retry post-hooks, deduped by (target field name, entity). */\n private addPendingTypeError(action: ReactiveAction): void {\n let byEntity = this.actionsPendingTypeErrors.get(action.r.name);\n if (!byEntity) this.actionsPendingTypeErrors.set(action.r.name, (byEntity = new Map()));\n byEntity.set(action.entity, action);\n }\n\n needsRecalc(kind: \"reactables\" | \"reactiveQueries\"): boolean {\n return kind === \"reactables\"\n ? this.#needsRecalc.populate || this.#needsRecalc.reaction || this.pendingTransitions.length > 0\n : this.#needsRecalc.query;\n }\n\n /**\n * Ends the creation of every new entity so far, before each loop of reactions.\n *\n * After this, assignments to those entities are transitions, i.e. an Approval created as `Requested`\n * that a reaction then auto-approves records `Requested -> Approved`.\n */\n private endTransitionCreations(): void {\n for (const entity of this.transitionCreations.keys()) this.postCreationWindowEntities.add(entity);\n this.transitionCreations.clear();\n }\n\n /**\n * Evaluates each pending transition's guards once, then dispatches its matching callbacks.\n *\n * I.e. Pending -> Signed -> Pending restores an advance's original status, but the Signed guard must still run.\n * Callbacks can append more transitions; taking one batch leaves those for the next reaction pass.\n * Each wave processes one step per entity, so populate requests batch across entities without\n * allowing an entity's next transition to overtake its previous callbacks.\n *\n * We might eventually treat A -> B -> A as an undo and ignore both transitions before reactions\n * have seen them. For now, each transition is meaningful, even when the final state is unchanged.\n */\n private async processPendingTransitions(): Promise<void> {\n // Ordinary reactions may have created more entities during this pass.\n this.endTransitionCreations();\n if (this.pendingTransitions.length === 0) return;\n const batch = this.pendingTransitions.splice(0);\n for (const wave of groupIntoEntityWaves(batch)) {\n const guardResults = await Promise.allSettled(wave.map((pending) => this.evaluateTransitionGuards(pending)));\n const errors = failIfAnyRejected(guardResults).flat();\n if (errors.length > 0) throw new ValidationErrors(errors);\n const callbackResults = await Promise.allSettled(wave.map((pending) => this.runTransitionCallbacks(pending)));\n failIfAnyRejected(callbackResults);\n }\n }\n\n /** Evaluates one transition's matching guards before any callbacks in its wave run. */\n private async evaluateTransitionGuards(pending: PendingTransition): Promise<ValidationError[]> {\n const { entity, fieldName, step } = pending;\n if (entity.isDeletedEntity || step.from === created) return [];\n const guards = getMetadata(entity)\n .transitions!.get(fieldName)!\n .guards.filter((guard) => matchesTransition(guard.matcher, step));\n const results = await Promise.allSettled(guards.map((guard) => invokeRule(entity, () => guard.run(entity, step))));\n return failIfAnyRejected(results).flat();\n }\n\n /** Runs one entity's callbacks in registration order after all guards in the wave have passed. */\n private async runTransitionCallbacks(pending: PendingTransition): Promise<void> {\n const { entity, fieldName, step } = pending;\n if (entity.isDeletedEntity) return;\n for (const callback of getMetadata(entity).transitions!.get(fieldName)!.callbacks) {\n if (entity.isDeletedEntity) break;\n if (!matchesTransition(callback.matcher, step)) continue;\n if (callback.phase === \"commit\") {\n this.pendingCommitTransitions.push({ entity, callback, step });\n } else {\n await callback.run(entity, this.em.ctx, step);\n }\n }\n }\n\n private getPending(r: Reactable): { todo: Set<Entity>; done: Set<Entity> } {\n let pending = this.pendingReactables.get(r);\n if (!pending) {\n pending = { todo: new Set(), done: new Set() };\n this.pendingReactables.set(r, pending);\n // The dirty field name is constant per reactable, so mark it once on the first\n // queue instead of paying the `getMetadata` + map lookup on every queued entity.\n this.getDirtyFields(getMetadata(r.cstr)).add(r.name);\n }\n return pending;\n }\n\n private getDirtyFields(meta: EntityMetadata): Set<string> {\n let dirty = this.dirtyFields.get(meta.tagName);\n if (!dirty) {\n dirty = new Set();\n this.dirtyFields.set(meta.tagName, dirty);\n }\n return dirty;\n }\n\n private getReactables(entity: Entity): Reactable[] {\n // If two books are getting merged, and so a normally-immutable `BookReview.book` is being changed,\n // then even normally-immutable fields need to be recalculated.\n const meta = getMetadata(entity);\n return getEmInternalApi(this.em).isMerging(entity) ? meta.reactablesIncludingReadOnly! : meta.reactables!;\n }\n\n private getReactablesByField(entity: Entity, fieldName: string): readonly Reactable[] {\n const meta = getMetadata(entity);\n const byField = getEmInternalApi(this.em).isMerging(entity)\n ? meta.reactablesIncludingReadOnlyByField!\n : meta.reactablesByField!;\n // Most fields have no reactables, so return a shared empty array instead of allocating one per set\n return byField.get(fieldName) ?? noReactables;\n }\n}\n\n/** A transition awaiting its guards and callbacks. */\ninterface PendingTransition {\n entity: Entity;\n fieldName: string;\n step: TransitionStep;\n}\n\n/** A matching commit callback whose transition's guards have already passed. */\ninterface PendingCommitCallback {\n entity: Entity;\n callback: TransitionCallback;\n step: TransitionStep;\n}\n\n/** Groups queued work into parallel waves while preserving each entity's input order. */\nfunction groupIntoEntityWaves<T extends { entity: Entity }>(batch: readonly T[]): T[][] {\n const waves: T[][] = [];\n const waveByEntity = new Map<Entity, number>();\n // I.e. [ba1 Signed, ba1 Paid, ba2 Signed] becomes [[ba1 Signed, ba2 Signed], [ba1 Paid]].\n for (const pending of batch) {\n const wave = waveByEntity.get(pending.entity) ?? 0;\n (waves[wave] ??= []).push(pending);\n waveByEntity.set(pending.entity, wave + 1);\n }\n return waves;\n}\n\n/** A shared frozen array for fields with no downstream reactables. */\nconst noReactables: readonly Reactable[] = Object.freeze([]);\n"],"mappings":"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"}
|
|
@@ -13,11 +13,26 @@ type ReactiveAction = {
|
|
|
13
13
|
*
|
|
14
14
|
* Source fields are usually regular/dumb primitives, i.e. `Author.age` is a source field
|
|
15
15
|
* of `Book.isPublic`, but derived fields can themselves be source fields as well.
|
|
16
|
+
* Guarded state transitions share this loop; their commit callbacks wait until SQL has been flushed.
|
|
16
17
|
*/
|
|
17
18
|
declare class ReactionsManager {
|
|
18
19
|
#private;
|
|
19
20
|
/** Stores all source `Reactables`s that have been marked for later traversal. */
|
|
20
21
|
private pendingReactables;
|
|
22
|
+
/** Queued state changes, including changes made by callbacks, are processed in reaction passes. */
|
|
23
|
+
private pendingTransitions;
|
|
24
|
+
/**
|
|
25
|
+
* Initial assignments are collapsed until reactions first see the entity.
|
|
26
|
+
*
|
|
27
|
+
* Each entity keeps one creation step per transition field. Each step is also in `pendingTransitions`, and
|
|
28
|
+
* later assignments update its `to` in place. An entity usually has one or two transition fields, so a
|
|
29
|
+
* short array is cheaper than a nested Map.
|
|
30
|
+
*/
|
|
31
|
+
private transitionCreations;
|
|
32
|
+
/** New entities that reactions have seen, so their assignments are transitions. */
|
|
33
|
+
private postCreationWindowEntities;
|
|
34
|
+
/** Only matching callbacks are retained until SQL and final validation finish. */
|
|
35
|
+
private pendingCommitTransitions;
|
|
21
36
|
/** Failed actions to retry post-hooks, deduped by target field name -> entity (rare error path). */
|
|
22
37
|
private actionsPendingTypeErrors;
|
|
23
38
|
/**
|
|
@@ -44,6 +59,21 @@ declare class ReactionsManager {
|
|
|
44
59
|
/** These are NPEs that *might* have been from invalid m2o fields, so we only throw them after validation. */
|
|
45
60
|
private suppressedTypeErrors;
|
|
46
61
|
constructor(em: EntityManager);
|
|
62
|
+
/**
|
|
63
|
+
* Called by `setField` before changing an enum field, to check and queue the transition.
|
|
64
|
+
*
|
|
65
|
+
* An entity has no previous state until Joist's reactions have seen it, so every assignment to a new
|
|
66
|
+
* entity until then is part of its creation. I.e. `em.create` then `entity.status = Open`, or a factory
|
|
67
|
+
* setting a default and then a `withStatus` value, or `em.findOrCreate` creating then upserting, all
|
|
68
|
+
* create the entity as `Open`. Creation isn't checked against the `transitions` table.
|
|
69
|
+
*/
|
|
70
|
+
queueTransition(entity: Entity, fieldName: string, from: unknown, to: unknown): void;
|
|
71
|
+
/** Forgets a field's creation transition after a factory assigns its trusted initial value. */
|
|
72
|
+
forgetCreationTransition(entity: Entity, fieldName: string): void;
|
|
73
|
+
/** Returns whether callbacks need a commit phase, even if there are no net SQL changes. */
|
|
74
|
+
hasPendingCommitTransitions(): boolean;
|
|
75
|
+
/** Runs the queued callbacks after all SQL and validation succeed, without rechecking guards. */
|
|
76
|
+
runCommitTransitions(): Promise<void>;
|
|
47
77
|
/**
|
|
48
78
|
* Queue all downstream reactive fields that depend on `fieldName` as a source field.
|
|
49
79
|
*
|
|
@@ -80,7 +110,7 @@ declare class ReactionsManager {
|
|
|
80
110
|
recalcPendingReactables(kind: "reactables" | "reactiveQueries"): Promise<void>;
|
|
81
111
|
recalcPendingTypeErrors(): Promise<void>;
|
|
82
112
|
get hasPendingTypeErrors(): boolean;
|
|
83
|
-
/** Clears
|
|
113
|
+
/** Clears pending reaction and transition work after a successful flush, including flushes with no SQL. */
|
|
84
114
|
clear(): void;
|
|
85
115
|
setLogger(logger: ReactionLogger | undefined): void;
|
|
86
116
|
clearSuppressedTypeErrors(): void;
|
|
@@ -88,6 +118,29 @@ declare class ReactionsManager {
|
|
|
88
118
|
/** Remembers a TypeError'd action to retry post-hooks, deduped by (target field name, entity). */
|
|
89
119
|
private addPendingTypeError;
|
|
90
120
|
needsRecalc(kind: "reactables" | "reactiveQueries"): boolean;
|
|
121
|
+
/**
|
|
122
|
+
* Ends the creation of every new entity so far, before each loop of reactions.
|
|
123
|
+
*
|
|
124
|
+
* After this, assignments to those entities are transitions, i.e. an Approval created as `Requested`
|
|
125
|
+
* that a reaction then auto-approves records `Requested -> Approved`.
|
|
126
|
+
*/
|
|
127
|
+
private endTransitionCreations;
|
|
128
|
+
/**
|
|
129
|
+
* Evaluates each pending transition's guards once, then dispatches its matching callbacks.
|
|
130
|
+
*
|
|
131
|
+
* I.e. Pending -> Signed -> Pending restores an advance's original status, but the Signed guard must still run.
|
|
132
|
+
* Callbacks can append more transitions; taking one batch leaves those for the next reaction pass.
|
|
133
|
+
* Each wave processes one step per entity, so populate requests batch across entities without
|
|
134
|
+
* allowing an entity's next transition to overtake its previous callbacks.
|
|
135
|
+
*
|
|
136
|
+
* We might eventually treat A -> B -> A as an undo and ignore both transitions before reactions
|
|
137
|
+
* have seen them. For now, each transition is meaningful, even when the final state is unchanged.
|
|
138
|
+
*/
|
|
139
|
+
private processPendingTransitions;
|
|
140
|
+
/** Evaluates one transition's matching guards before any callbacks in its wave run. */
|
|
141
|
+
private evaluateTransitionGuards;
|
|
142
|
+
/** Runs one entity's callbacks in registration order after all guards in the wave have passed. */
|
|
143
|
+
private runTransitionCallbacks;
|
|
91
144
|
private getPending;
|
|
92
145
|
private getDirtyFields;
|
|
93
146
|
private getReactables;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ReactionsManager.d.cts","names":[],"sources":["../../src/reactivity/ReactionsManager.ts"],"mappings":";;;;;
|
|
1
|
+
{"version":3,"file":"ReactionsManager.d.cts","names":[],"sources":["../../src/reactivity/ReactionsManager.ts"],"mappings":";;;;;KAkBY;EAAmB,GAAG;EAAW,QAAQ;;;;;;;;;;cASxC;;;UAEH;;UAEA;;;;;;;;UAQA;;UAEA;;UAEA;;UAEA;;;;;;;;;;UAUA;;;;;;;;;UASA;EAGR,QAAQ;UACA;;UAEA;EAER,YAAY,IAAI;;;;;;;;;EAYhB,gBAAgB,QAAQ,QAAQ,mBAAmB,eAAe;;EA4BlE,yBAAyB,QAAQ,QAAQ;;EAUzC;;EAKM,wBAAwB;;;;;;;;EAqB9B,0BAA0B,QAAQ,QAAQ;;EAkB1C,4BAA4B,QAAQ,QAAQ;;EAyB5C,yBAAyB,QAAQ,QAAQ;;;;;;;;;;;;;EAoBzC,qBAAqB,QAAQ,QAAQ;EAIrC;;;;;;;;EAWM,wBAAwB,yCAAsC;EAuH9D,2BAAuB;MA4BzB;;EAKJ;EAUA,UAAU,QAAQ;EAIlB;EAIA;;UAYQ;EAMR,YAAY;;;;;;;UAYJ;;;;;;;;;;;;UAgBM;;UAeA;;UAWA;UAcN;UAYA;UASA;UAOA"}
|
|
@@ -13,11 +13,26 @@ type ReactiveAction = {
|
|
|
13
13
|
*
|
|
14
14
|
* Source fields are usually regular/dumb primitives, i.e. `Author.age` is a source field
|
|
15
15
|
* of `Book.isPublic`, but derived fields can themselves be source fields as well.
|
|
16
|
+
* Guarded state transitions share this loop; their commit callbacks wait until SQL has been flushed.
|
|
16
17
|
*/
|
|
17
18
|
declare class ReactionsManager {
|
|
18
19
|
#private;
|
|
19
20
|
/** Stores all source `Reactables`s that have been marked for later traversal. */
|
|
20
21
|
private pendingReactables;
|
|
22
|
+
/** Queued state changes, including changes made by callbacks, are processed in reaction passes. */
|
|
23
|
+
private pendingTransitions;
|
|
24
|
+
/**
|
|
25
|
+
* Initial assignments are collapsed until reactions first see the entity.
|
|
26
|
+
*
|
|
27
|
+
* Each entity keeps one creation step per transition field. Each step is also in `pendingTransitions`, and
|
|
28
|
+
* later assignments update its `to` in place. An entity usually has one or two transition fields, so a
|
|
29
|
+
* short array is cheaper than a nested Map.
|
|
30
|
+
*/
|
|
31
|
+
private transitionCreations;
|
|
32
|
+
/** New entities that reactions have seen, so their assignments are transitions. */
|
|
33
|
+
private postCreationWindowEntities;
|
|
34
|
+
/** Only matching callbacks are retained until SQL and final validation finish. */
|
|
35
|
+
private pendingCommitTransitions;
|
|
21
36
|
/** Failed actions to retry post-hooks, deduped by target field name -> entity (rare error path). */
|
|
22
37
|
private actionsPendingTypeErrors;
|
|
23
38
|
/**
|
|
@@ -44,6 +59,21 @@ declare class ReactionsManager {
|
|
|
44
59
|
/** These are NPEs that *might* have been from invalid m2o fields, so we only throw them after validation. */
|
|
45
60
|
private suppressedTypeErrors;
|
|
46
61
|
constructor(em: EntityManager);
|
|
62
|
+
/**
|
|
63
|
+
* Called by `setField` before changing an enum field, to check and queue the transition.
|
|
64
|
+
*
|
|
65
|
+
* An entity has no previous state until Joist's reactions have seen it, so every assignment to a new
|
|
66
|
+
* entity until then is part of its creation. I.e. `em.create` then `entity.status = Open`, or a factory
|
|
67
|
+
* setting a default and then a `withStatus` value, or `em.findOrCreate` creating then upserting, all
|
|
68
|
+
* create the entity as `Open`. Creation isn't checked against the `transitions` table.
|
|
69
|
+
*/
|
|
70
|
+
queueTransition(entity: Entity, fieldName: string, from: unknown, to: unknown): void;
|
|
71
|
+
/** Forgets a field's creation transition after a factory assigns its trusted initial value. */
|
|
72
|
+
forgetCreationTransition(entity: Entity, fieldName: string): void;
|
|
73
|
+
/** Returns whether callbacks need a commit phase, even if there are no net SQL changes. */
|
|
74
|
+
hasPendingCommitTransitions(): boolean;
|
|
75
|
+
/** Runs the queued callbacks after all SQL and validation succeed, without rechecking guards. */
|
|
76
|
+
runCommitTransitions(): Promise<void>;
|
|
47
77
|
/**
|
|
48
78
|
* Queue all downstream reactive fields that depend on `fieldName` as a source field.
|
|
49
79
|
*
|
|
@@ -80,7 +110,7 @@ declare class ReactionsManager {
|
|
|
80
110
|
recalcPendingReactables(kind: "reactables" | "reactiveQueries"): Promise<void>;
|
|
81
111
|
recalcPendingTypeErrors(): Promise<void>;
|
|
82
112
|
get hasPendingTypeErrors(): boolean;
|
|
83
|
-
/** Clears
|
|
113
|
+
/** Clears pending reaction and transition work after a successful flush, including flushes with no SQL. */
|
|
84
114
|
clear(): void;
|
|
85
115
|
setLogger(logger: ReactionLogger | undefined): void;
|
|
86
116
|
clearSuppressedTypeErrors(): void;
|
|
@@ -88,6 +118,29 @@ declare class ReactionsManager {
|
|
|
88
118
|
/** Remembers a TypeError'd action to retry post-hooks, deduped by (target field name, entity). */
|
|
89
119
|
private addPendingTypeError;
|
|
90
120
|
needsRecalc(kind: "reactables" | "reactiveQueries"): boolean;
|
|
121
|
+
/**
|
|
122
|
+
* Ends the creation of every new entity so far, before each loop of reactions.
|
|
123
|
+
*
|
|
124
|
+
* After this, assignments to those entities are transitions, i.e. an Approval created as `Requested`
|
|
125
|
+
* that a reaction then auto-approves records `Requested -> Approved`.
|
|
126
|
+
*/
|
|
127
|
+
private endTransitionCreations;
|
|
128
|
+
/**
|
|
129
|
+
* Evaluates each pending transition's guards once, then dispatches its matching callbacks.
|
|
130
|
+
*
|
|
131
|
+
* I.e. Pending -> Signed -> Pending restores an advance's original status, but the Signed guard must still run.
|
|
132
|
+
* Callbacks can append more transitions; taking one batch leaves those for the next reaction pass.
|
|
133
|
+
* Each wave processes one step per entity, so populate requests batch across entities without
|
|
134
|
+
* allowing an entity's next transition to overtake its previous callbacks.
|
|
135
|
+
*
|
|
136
|
+
* We might eventually treat A -> B -> A as an undo and ignore both transitions before reactions
|
|
137
|
+
* have seen them. For now, each transition is meaningful, even when the final state is unchanged.
|
|
138
|
+
*/
|
|
139
|
+
private processPendingTransitions;
|
|
140
|
+
/** Evaluates one transition's matching guards before any callbacks in its wave run. */
|
|
141
|
+
private evaluateTransitionGuards;
|
|
142
|
+
/** Runs one entity's callbacks in registration order after all guards in the wave have passed. */
|
|
143
|
+
private runTransitionCallbacks;
|
|
91
144
|
private getPending;
|
|
92
145
|
private getDirtyFields;
|
|
93
146
|
private getReactables;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ReactionsManager.d.mts","names":[],"sources":["../../src/reactivity/ReactionsManager.ts"],"mappings":";;;;;
|
|
1
|
+
{"version":3,"file":"ReactionsManager.d.mts","names":[],"sources":["../../src/reactivity/ReactionsManager.ts"],"mappings":";;;;;KAkBY;EAAmB,GAAG;EAAW,QAAQ;;;;;;;;;;cASxC;;;UAEH;;UAEA;;;;;;;;UAQA;;UAEA;;UAEA;;UAEA;;;;;;;;;;UAUA;;;;;;;;;UASA;EAGR,QAAQ;UACA;;UAEA;EAER,YAAY,IAAI;;;;;;;;;EAYhB,gBAAgB,QAAQ,QAAQ,mBAAmB,eAAe;;EA4BlE,yBAAyB,QAAQ,QAAQ;;EAUzC;;EAKM,wBAAwB;;;;;;;;EAqB9B,0BAA0B,QAAQ,QAAQ;;EAkB1C,4BAA4B,QAAQ,QAAQ;;EAyB5C,yBAAyB,QAAQ,QAAQ;;;;;;;;;;;;;EAoBzC,qBAAqB,QAAQ,QAAQ;EAIrC;;;;;;;;EAWM,wBAAwB,yCAAsC;EAuH9D,2BAAuB;MA4BzB;;EAKJ;EAUA,UAAU,QAAQ;EAIlB;EAIA;;UAYQ;EAMR,YAAY;;;;;;;UAYJ;;;;;;;;;;;;UAgBM;;UAeA;;UAWA;UAcN;UAYA;UASA;UAOA"}
|