skynet-graph 1.2.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/CONTRIBUTING.md +22 -0
- package/LICENSE +661 -0
- package/README.md +245 -0
- package/bin/sg +20 -0
- package/concepts/README.md +23 -0
- package/concepts/_substrate/Claim/Confidence/Trusted.json +6 -0
- package/concepts/_substrate/Claim/Confidence.json +10 -0
- package/concepts/_substrate/Claim/Freshness/Stale.json +7 -0
- package/concepts/_substrate/Claim/Freshness.json +5 -0
- package/concepts/_substrate/Claim/Verification/Refuted.json +6 -0
- package/concepts/_substrate/Claim/Verification.json +6 -0
- package/concepts/_substrate/Claim.json +4 -0
- package/concepts/_substrate/Frontier/InBeam.json +6 -0
- package/concepts/_substrate/Frontier/Stuck.json +6 -0
- package/concepts/_substrate/Frontier.json +5 -0
- package/concepts/_substrate/Intake/ToTask.json +22 -0
- package/concepts/_substrate/Intake/Typed.json +10 -0
- package/concepts/_substrate/Intake.json +27 -0
- package/concepts/_substrate/Task/Answer.json +12 -0
- package/concepts/_substrate/Task/Complexity/Atomic.json +5 -0
- package/concepts/_substrate/Task/Complexity/Compound/Expansion.json +7 -0
- package/concepts/_substrate/Task/Complexity/Compound.json +6 -0
- package/concepts/_substrate/Task/Complexity.json +6 -0
- package/concepts/_substrate/Task/EvalComplexity.json +13 -0
- package/concepts/_substrate/Task/ReportUp.json +6 -0
- package/concepts/_substrate/Task/Rollup.json +10 -0
- package/concepts/_substrate/Task.json +5 -0
- package/concepts/clinical/Diagnosis/Medication.json +6 -0
- package/concepts/clinical/Diagnosis.json +15 -0
- package/concepts/clinical/Observation/LabValue/OutOfRange.json +6 -0
- package/concepts/clinical/Observation/LabValue.json +5 -0
- package/concepts/clinical/Observation.json +6 -0
- package/concepts/common/Document/pathBasket.json +6 -0
- package/concepts/common/Document.json +3 -0
- package/concepts/common/Edge/Distance.json +13 -0
- package/concepts/common/Edge/Stay/LongStay.json +8 -0
- package/concepts/common/Edge/Stay.json +8 -0
- package/concepts/common/Edge/Travel/LongTravel.json +16 -0
- package/concepts/common/Edge/Travel/ShortTravel.json +11 -0
- package/concepts/common/Edge/Travel/targetNode.json +7 -0
- package/concepts/common/Edge/Travel.json +7 -0
- package/concepts/common/Edge.json +3 -0
- package/concepts/common/Vertice.json +3 -0
- package/concepts/supply/Fulfillment.json +10 -0
- package/concepts/supply/Inventory/Reorder.json +7 -0
- package/concepts/supply/Inventory.json +5 -0
- package/concepts/supply/Procurement/SupplierConfirm.json +6 -0
- package/concepts/supply/Procurement.json +8 -0
- package/concepts/supply/Transport.json +5 -0
- package/doc/API.md +556 -0
- package/doc/CAPABILITIES.md +373 -0
- package/doc/MODELISATION.md +511 -0
- package/doc/architecture.md +267 -0
- package/doc/concept-as-graph.md +379 -0
- package/doc/concept-learning.md +168 -0
- package/doc/creative-loop-map.md +202 -0
- package/doc/original-2016-doc.md +148 -0
- package/doc/usage.md +501 -0
- package/lib/authoring/abstract.js +401 -0
- package/lib/authoring/abstraction.js +190 -0
- package/lib/authoring/adapt.js +393 -0
- package/lib/authoring/ancestry.js +216 -0
- package/lib/authoring/author.js +147 -0
- package/lib/authoring/bounded-merge.js +57 -0
- package/lib/authoring/canon.js +158 -0
- package/lib/authoring/clock.js +123 -0
- package/lib/authoring/combinator.js +92 -0
- package/lib/authoring/compete.js +76 -0
- package/lib/authoring/compose-hotspot.js +314 -0
- package/lib/authoring/compress.js +279 -0
- package/lib/authoring/concept-net.js +254 -0
- package/lib/authoring/concepts.js +66 -0
- package/lib/authoring/context-project.js +277 -0
- package/lib/authoring/contract.js +476 -0
- package/lib/authoring/corpus-pack.js +125 -0
- package/lib/authoring/cost-probe.js +86 -0
- package/lib/authoring/crystallize.js +555 -0
- package/lib/authoring/dag-decompose.js +256 -0
- package/lib/authoring/dataset-adapter.js +253 -0
- package/lib/authoring/debug-provider.js +92 -0
- package/lib/authoring/decompose.js +343 -0
- package/lib/authoring/emittability.js +236 -0
- package/lib/authoring/equilibrium.js +160 -0
- package/lib/authoring/extract.js +153 -0
- package/lib/authoring/forest.js +55 -0
- package/lib/authoring/forge-fallback.js +116 -0
- package/lib/authoring/givens.js +121 -0
- package/lib/authoring/glossary.js +177 -0
- package/lib/authoring/grammar-graph.js +191 -0
- package/lib/authoring/granularity.js +80 -0
- package/lib/authoring/graph-net.js +108 -0
- package/lib/authoring/ground.js +148 -0
- package/lib/authoring/higher-order.js +85 -0
- package/lib/authoring/hotspot.js +132 -0
- package/lib/authoring/hysteresis.js +85 -0
- package/lib/authoring/index.js +106 -0
- package/lib/authoring/lattice-morphism.js +61 -0
- package/lib/authoring/lattice-pack.js +148 -0
- package/lib/authoring/leaf-io.js +77 -0
- package/lib/authoring/library.js +156 -0
- package/lib/authoring/lifecycle.js +92 -0
- package/lib/authoring/loop.js +193 -0
- package/lib/authoring/master-loop.js +132 -0
- package/lib/authoring/mdl.js +209 -0
- package/lib/authoring/memo-stability.js +122 -0
- package/lib/authoring/method-explorer.js +138 -0
- package/lib/authoring/method-pack.js +181 -0
- package/lib/authoring/method.js +202 -0
- package/lib/authoring/mine.js +510 -0
- package/lib/authoring/mount.js +119 -0
- package/lib/authoring/negotiate.js +100 -0
- package/lib/authoring/parametric.js +139 -0
- package/lib/authoring/reaggregate.js +75 -0
- package/lib/authoring/rebalance.js +157 -0
- package/lib/authoring/recall.js +135 -0
- package/lib/authoring/registry.js +350 -0
- package/lib/authoring/relearn.js +147 -0
- package/lib/authoring/retention.js +89 -0
- package/lib/authoring/segment-proxy.js +114 -0
- package/lib/authoring/serve-leaf.js +64 -0
- package/lib/authoring/slot-aware-serve.js +77 -0
- package/lib/authoring/sound-invoke.js +59 -0
- package/lib/authoring/split-serve.js +74 -0
- package/lib/authoring/ste.js +239 -0
- package/lib/authoring/stock.js +101 -0
- package/lib/authoring/store.js +94 -0
- package/lib/authoring/supervise.js +138 -0
- package/lib/authoring/support.js +131 -0
- package/lib/authoring/task-mirror.js +51 -0
- package/lib/authoring/typed-loop.js +160 -0
- package/lib/authoring/validate.js +569 -0
- package/lib/authoring/widen.js +88 -0
- package/lib/combos/appliance.js +189 -0
- package/lib/combos/critique.js +377 -0
- package/lib/combos/defaults.js +86 -0
- package/lib/combos/durable-runner.js +115 -0
- package/lib/combos/forge.js +209 -0
- package/lib/combos/index.js +86 -0
- package/lib/combos/learning-library.js +253 -0
- package/lib/combos/mixture-serve.js +193 -0
- package/lib/combos/plan-loop.js +192 -0
- package/lib/combos/proxy-cache.js +272 -0
- package/lib/combos/self-mod.js +94 -0
- package/lib/durable/audit.js +102 -0
- package/lib/durable/checkpoint-store.js +550 -0
- package/lib/durable/fold.js +62 -0
- package/lib/durable/interpreter.js +242 -0
- package/lib/durable/xlate.js +185 -0
- package/lib/graph/Graph.js +2486 -0
- package/lib/graph/expr.js +307 -0
- package/lib/graph/index.js +26 -0
- package/lib/graph/log.js +134 -0
- package/lib/graph/objects/Concept.js +382 -0
- package/lib/graph/objects/Entity.js +503 -0
- package/lib/graph/objects/Node.js +57 -0
- package/lib/graph/objects/PathMap.js +557 -0
- package/lib/graph/objects/Segment.js +100 -0
- package/lib/graph/tasks/stabilize.js +59 -0
- package/lib/graph/tasks/taskflow.js +247 -0
- package/lib/index.js +122 -0
- package/lib/load.js +132 -0
- package/lib/providers/backends.js +117 -0
- package/lib/providers/borderline.js +129 -0
- package/lib/providers/cache.js +226 -0
- package/lib/providers/canonicalize.js +198 -0
- package/lib/providers/constat.js +107 -0
- package/lib/providers/geo.js +53 -0
- package/lib/providers/index.js +116 -0
- package/lib/providers/intake.js +263 -0
- package/lib/providers/llm-local.js +88 -0
- package/lib/providers/llm.js +236 -0
- package/lib/providers/local-host.js +197 -0
- package/lib/providers/merge-consistency.js +162 -0
- package/lib/providers/nogood.js +149 -0
- package/lib/providers/reason-loop.js +223 -0
- package/lib/providers/semiring.js +285 -0
- package/lib/providers/solver-fork.js +163 -0
- package/lib/providers/stats.js +228 -0
- package/lib/providers/verify.js +120 -0
- package/lib/runtime/index.js +135 -0
- package/lib/runtime/invoke-pool.js +92 -0
- package/lib/runtime/protocol.js +210 -0
- package/lib/runtime/transport-socket.js +77 -0
- package/lib/runtime/worker-entry.js +31 -0
- package/lib/sg/cli.js +601 -0
- package/lib/sg/log-sinks.js +214 -0
- package/lib/sg/mcp.js +395 -0
- package/lib/sg/proxy-run.js +66 -0
- package/lib/sg/serve-demo.js +185 -0
- package/lib/sg/serve.js +205 -0
- package/lib/sg/trace.js +118 -0
- package/lib/studio/protocol.js +45 -0
- package/lib/studio/public/app.js +310 -0
- package/lib/studio/public/components/ConceptEditor.js +43 -0
- package/lib/studio/public/components/ConceptTree.js +43 -0
- package/lib/studio/public/components/CorpusPanel.js +68 -0
- package/lib/studio/public/components/ForkTree.js +36 -0
- package/lib/studio/public/components/GrammarGraph.js +99 -0
- package/lib/studio/public/components/GraphCanvas.js +156 -0
- package/lib/studio/public/components/Inspector.js +48 -0
- package/lib/studio/public/components/LearningPanel.js +85 -0
- package/lib/studio/public/components/PromptConsole.js +46 -0
- package/lib/studio/public/components/ProviderTrace.js +39 -0
- package/lib/studio/public/components/SessionSplit.js +47 -0
- package/lib/studio/public/components/TilingOverlay.js +40 -0
- package/lib/studio/public/components/Timeline.js +57 -0
- package/lib/studio/public/index.html +26 -0
- package/lib/studio/public/styles.css +185 -0
- package/lib/studio/public/ws.js +58 -0
- package/lib/studio/server.js +130 -0
- package/lib/studio/session.js +397 -0
- package/lib/studio/studio.js +149 -0
- package/package.json +42 -0
|
@@ -0,0 +1,2486 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026 Nathanael Braun
|
|
3
|
+
* @author : Nathanael BRAUN <pp9ping@gmail.com>
|
|
4
|
+
*
|
|
5
|
+
* This program is free software: you can redistribute it and/or modify
|
|
6
|
+
* it under the terms of the GNU Affero General Public License as published by
|
|
7
|
+
* the Free Software Foundation, either version 3 of the License, or
|
|
8
|
+
* (at your option) any later version.
|
|
9
|
+
*
|
|
10
|
+
* This program is distributed in the hope that it will be useful,
|
|
11
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
12
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
13
|
+
* GNU Affero General Public License for more details.
|
|
14
|
+
*
|
|
15
|
+
* You should have received a copy of the GNU Affero General Public License
|
|
16
|
+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
var TaskFlow = require("./tasks/taskflow");
|
|
20
|
+
var Node = require("./objects/Node");
|
|
21
|
+
var Segment = require("./objects/Segment");
|
|
22
|
+
var PathMap = require("./objects/PathMap");
|
|
23
|
+
var Concept = require("./objects/Concept");
|
|
24
|
+
var Entity = require("./objects/Entity");
|
|
25
|
+
//import conceptMap from "../concepts";
|
|
26
|
+
|
|
27
|
+
var isObject = require('is').object;
|
|
28
|
+
var isArray = require('is').array;
|
|
29
|
+
var isFunction = require('is').fn;
|
|
30
|
+
var isString = require('is').string;
|
|
31
|
+
var { compileExpression } = require('./expr');
|
|
32
|
+
|
|
33
|
+
// Wrap a query (string | array | fn) into a predicate fn(scope) that resolves
|
|
34
|
+
// $refs via scope.getRef — replaces the old `new Function` query compiler.
|
|
35
|
+
function compileScopeQuery ( query, emptyValue ) {
|
|
36
|
+
if ( isFunction(query) ) return query;
|
|
37
|
+
var _q = compileExpression(query, { empty: emptyValue });
|
|
38
|
+
return function ( scope ) {
|
|
39
|
+
return _q(function ( ref ) { return scope.getRef(ref); });
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
var shortid = require('shortid');
|
|
43
|
+
var dmerge = require('deepmerge');
|
|
44
|
+
var intersect = require('intersect');
|
|
45
|
+
var arrayDiffer = require('array-differ');
|
|
46
|
+
var { createLogger, defaultLogger } = require('./log');
|
|
47
|
+
// Module-level fallback for the rare spots with no graph in scope. Inside methods
|
|
48
|
+
// we rebind `var debug = this._log` (or `me._log`) so logs route to the in-scope
|
|
49
|
+
// graph's own logger (per-graph, captured by a host's injected sink). See init().
|
|
50
|
+
var debug = defaultLogger;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* serialized format :
|
|
54
|
+
* {
|
|
55
|
+
* spatialEP : (nodeid)
|
|
56
|
+
* conceptMaps : [
|
|
57
|
+
* // ... any serialized concept box (nodes/edge or docs) not serialized
|
|
58
|
+
* ],
|
|
59
|
+
* // or
|
|
60
|
+
* nodes : [
|
|
61
|
+
* {
|
|
62
|
+
* _id : ...
|
|
63
|
+
* ConceptKey1 : true,
|
|
64
|
+
* ConceptKey2 : true,//...
|
|
65
|
+
* }
|
|
66
|
+
* ]
|
|
67
|
+
* segments : [
|
|
68
|
+
* {
|
|
69
|
+
* _id : ...
|
|
70
|
+
* originNode : {node _id}
|
|
71
|
+
* targetNode : {node _id}
|
|
72
|
+
* }
|
|
73
|
+
* ]
|
|
74
|
+
* ]
|
|
75
|
+
* }
|
|
76
|
+
*
|
|
77
|
+
*
|
|
78
|
+
* templates format :
|
|
79
|
+
* example : from adding Airports
|
|
80
|
+
[
|
|
81
|
+
{// **************** add start to airport segment
|
|
82
|
+
"Segment": true,
|
|
83
|
+
"$originNode": "_parent:originNode",
|
|
84
|
+
|
|
85
|
+
"targetNode": "nearbyOriginAirport"
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
// **************** add nearby origin airport concept node
|
|
89
|
+
"_id": "nearbyOriginAirport",
|
|
90
|
+
"Node": true,
|
|
91
|
+
"isAirport": true,
|
|
92
|
+
"$nearTo": "_parent:originNode"// ref a value or cbox
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
// **************** add long travel flight segment
|
|
96
|
+
"Segment": true,
|
|
97
|
+
"$_id": "_parent", // add concepts/keys to _parent, if no $_id -> original object will be kept
|
|
98
|
+
"LongTravel": true,//
|
|
99
|
+
"Distance":null,// should recalculate distance as soon as nearby airport have Position
|
|
100
|
+
"originNode": "nearbyOriginAirport",
|
|
101
|
+
"targetNode": "nearbyTargetAirport"
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
// **************** add nearby target airport concept node
|
|
105
|
+
"_id": "nearbyTargetAirport",
|
|
106
|
+
"Node": true,
|
|
107
|
+
"isAirport": true,
|
|
108
|
+
"$nearTo": "_parent:targetNode"
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
// **************** add final nearby target airport TO target place
|
|
112
|
+
"Segment": true,
|
|
113
|
+
"originNode": "nearbyTargetAirport",
|
|
114
|
+
"$targetNode": "_parent:targetNode"
|
|
115
|
+
}
|
|
116
|
+
]
|
|
117
|
+
|
|
118
|
+
*/
|
|
119
|
+
function Graph() {
|
|
120
|
+
this.init(...arguments);
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
//Graph._providers = require("../providers");
|
|
124
|
+
|
|
125
|
+
Graph.PathMap = PathMap;
|
|
126
|
+
Graph.Entity = Entity;
|
|
127
|
+
Graph.Concept = Concept;
|
|
128
|
+
|
|
129
|
+
Graph.prototype = {
|
|
130
|
+
static: Graph,
|
|
131
|
+
cfg : {
|
|
132
|
+
label : "graph",
|
|
133
|
+
autoMount : true,
|
|
134
|
+
isMaster : true,
|
|
135
|
+
onStabilize : undefined, // should trigger synchronisation between graphs
|
|
136
|
+
onConceptApply: undefined, // (record) => {} : concept-apply trace sink (see traceProvider)
|
|
137
|
+
conceptSets : ["common"],
|
|
138
|
+
defaultContext: "UserRecord",
|
|
139
|
+
bagRefManagers: {
|
|
140
|
+
caipi: {
|
|
141
|
+
test: /^db\:(.+)$/,
|
|
142
|
+
int : {
|
|
143
|
+
get( refId, cb ) {
|
|
144
|
+
refId = refId.split('#');
|
|
145
|
+
// `App/db` is an optional HOST module (not shipped by the engine). The
|
|
146
|
+
// default 'caipi' manager degrades gracefully (cb error) if the host hasn't
|
|
147
|
+
// provided it — pass cfg.bagRefManagers to supply your own data source.
|
|
148
|
+
var db;
|
|
149
|
+
try { db = require('App/db'); }
|
|
150
|
+
catch ( e ) { return cb(new Error("bagRefManager 'caipi': host module 'App/db' not available; pass cfg.bagRefManagers"), null); }
|
|
151
|
+
db.get(refId[0], refId[1], cb);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
/**
|
|
158
|
+
*
|
|
159
|
+
* @param serialized
|
|
160
|
+
* @param conf
|
|
161
|
+
*/
|
|
162
|
+
init: function ( record, conf, conceptMap ) {
|
|
163
|
+
var concepts = {}, me = this,
|
|
164
|
+
serialized;
|
|
165
|
+
if ( isString(record.graph) ) {
|
|
166
|
+
serialized = JSON.parse(record.graph);
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
serialized = record;
|
|
170
|
+
record = {
|
|
171
|
+
lastRev: serialized.lastRev,
|
|
172
|
+
graph : JSON.stringify(record),
|
|
173
|
+
//bagRefs: {}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
this._triggeredCast = {};
|
|
178
|
+
this._stabilizing = false;// true while a stabilization pass is in flight (see stabilize.js / _applyStabilized)
|
|
179
|
+
this._pendingStructural = [];// add/patchConcept issued mid-stabilize, drained at the quiescent _loopTF boundary (#11.a)
|
|
180
|
+
this._pendingRollback = null;// rollbackTo issued mid-stabilize, applied at the quiescent boundary (#11.c.4)
|
|
181
|
+
this.cfg = { ...this.cfg, ...conf };
|
|
182
|
+
// One logger per graph (replaces the old console indirection). cfg.logger lets a
|
|
183
|
+
// host inject its own; else build one at cfg.logLevel (or env SG_LOG_LEVEL), with
|
|
184
|
+
// cfg.onLog as a convenience sink. Exposed as graph.logger (see below the prototype).
|
|
185
|
+
this._log = this.cfg.logger || createLogger({ label: this.cfg.label, level: this.cfg.logLevel, onRecord: this.cfg.onLog });
|
|
186
|
+
this._applyId = 0;// monotonic id minted per concept-apply: correlates an apply's logs with its trace record (see Concept.applyTo)
|
|
187
|
+
this._applyCount = {};// per-(target/concept) apply tally within an episode; reset on settle (#11.c.1)
|
|
188
|
+
this._applyCap = this.cfg.applyCap || 1000;// oscillation backstop ceiling (per target×concept per episode)
|
|
189
|
+
this.cfg.conceptSets.map(( k ) => concepts = dmerge(concepts, conceptMap[k]));
|
|
190
|
+
me._conceptMap = conceptMap;// kept so fork() can seed children with the same library
|
|
191
|
+
me._conceptLib = {};
|
|
192
|
+
me._syncTokens = {};
|
|
193
|
+
me._syncTokensList = [];
|
|
194
|
+
|
|
195
|
+
this._lastSyncRecord = record;
|
|
196
|
+
|
|
197
|
+
this._rootConcept = new Concept(concepts, me);
|
|
198
|
+
this._mapsByConcept = {};
|
|
199
|
+
this._statsByProvider = {};
|
|
200
|
+
this._bagRefsByRefId = {};
|
|
201
|
+
this._on = {};
|
|
202
|
+
this._revs = [];
|
|
203
|
+
this._revByIds = {};
|
|
204
|
+
this._rev = serialized.lastRev || 0;
|
|
205
|
+
me._triggeredCastCount = 0;
|
|
206
|
+
// TaskFlow (will handle all the graph tasks/mutations)
|
|
207
|
+
|
|
208
|
+
this._taskFlow = new TaskFlow(
|
|
209
|
+
[
|
|
210
|
+
require('./tasks/stabilize')
|
|
211
|
+
],
|
|
212
|
+
this
|
|
213
|
+
).then(this._loopTF);
|
|
214
|
+
this._preloadBagRefs(
|
|
215
|
+
serialized.bagRefs || {},
|
|
216
|
+
() => {
|
|
217
|
+
serialized && this.mount(serialized);
|
|
218
|
+
|
|
219
|
+
if ( this.cfg.autoMount ) {
|
|
220
|
+
this._taskFlow.run();
|
|
221
|
+
me._running = true;
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
setTimeout(this._applyStabilized.bind(this));
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
);
|
|
228
|
+
},
|
|
229
|
+
/**
|
|
230
|
+
*
|
|
231
|
+
* @param newRefs {"$db:id_from_somwhere":{count:1}}
|
|
232
|
+
* @param cb
|
|
233
|
+
* @private
|
|
234
|
+
*/
|
|
235
|
+
_preloadBagRefs( newRefs, cb ) {
|
|
236
|
+
// internal : {"$db:id_from_somwhere":{count:1, lastUpdated:tm, watch:tm, record}}
|
|
237
|
+
var debug = this._log;// per-graph logger; the nested arrows below capture it
|
|
238
|
+
let refsMap = this._bagRefsByRefId,
|
|
239
|
+
mngrs = this.cfg.bagRefManagers,
|
|
240
|
+
running = 1,
|
|
241
|
+
tm,
|
|
242
|
+
refs = Object.keys(newRefs),
|
|
243
|
+
check = () => {
|
|
244
|
+
debug.warn("timeout Preloading refs", newRefs);
|
|
245
|
+
tm = setTimeout(check, 5000);
|
|
246
|
+
},
|
|
247
|
+
done = () => {
|
|
248
|
+
if ( !--running ) {
|
|
249
|
+
//refs.length && debug.log("done Preloading refs", refs);
|
|
250
|
+
clearTimeout(tm)
|
|
251
|
+
//this._taskFlow.release()
|
|
252
|
+
cb()
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
//this._taskFlow.wait()
|
|
256
|
+
tm = setTimeout(check, 5000);
|
|
257
|
+
//refs.length && debug.log("Preloading ", refs);
|
|
258
|
+
refs.forEach(
|
|
259
|
+
id => {
|
|
260
|
+
let refMngrId = id && this._isBagRefs(id), v;
|
|
261
|
+
if ( refMngrId && !refsMap[id] ) {
|
|
262
|
+
v = ('' + id).match(mngrs[refMngrId].test)
|
|
263
|
+
//refsMap[id] = refsMap[id] || {
|
|
264
|
+
// count: 0
|
|
265
|
+
//};
|
|
266
|
+
running++;
|
|
267
|
+
mngrs[refMngrId].int.get(
|
|
268
|
+
v[1] || id,
|
|
269
|
+
( e, r ) => {
|
|
270
|
+
if ( e )
|
|
271
|
+
debug.error("can't retrieve bagRef %s", id);
|
|
272
|
+
refsMap[id] = refsMap[id] || {
|
|
273
|
+
count: 0
|
|
274
|
+
};
|
|
275
|
+
refsMap[id].count++;
|
|
276
|
+
refsMap[id].lastUpdated = Date.now();
|
|
277
|
+
refsMap[id].record = r || { name: "Error" };
|
|
278
|
+
done()
|
|
279
|
+
}
|
|
280
|
+
)
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
done();
|
|
286
|
+
},
|
|
287
|
+
_isBagRefs( id ) {
|
|
288
|
+
if ( id == null || id === '' ) return false;// fast-path: a ref hop's key is most often absent — skip the per-manager regex loop on the hot path
|
|
289
|
+
let refsMap = this._bagRefsByRefId,
|
|
290
|
+
mngrs = this.cfg.bagRefManagers,
|
|
291
|
+
mKeys = Object.keys(mngrs);
|
|
292
|
+
for ( var i = 0, v; i < mKeys.length; i++ ) {
|
|
293
|
+
|
|
294
|
+
v = ('' + id).match(mngrs[mKeys[i]].test)
|
|
295
|
+
if ( v ) {
|
|
296
|
+
return mKeys[i]
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return false;
|
|
300
|
+
},
|
|
301
|
+
// -------------------------------------------------------------------------- core
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Make the stabilisation taskflow loop until theres no more unstable items
|
|
305
|
+
* @param me
|
|
306
|
+
* @param flow
|
|
307
|
+
* @private
|
|
308
|
+
*/
|
|
309
|
+
_loopTF : function ( me, flow ) {
|
|
310
|
+
if ( me._dead ) return;
|
|
311
|
+
var debug = me._log;// per-graph logger (captured by the setTimeout closure below)
|
|
312
|
+
// flow.running=false;
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
// me._running = me.cfg.autoMount;
|
|
316
|
+
setTimeout(function () {// loop
|
|
317
|
+
if ( me._dead ) return;
|
|
318
|
+
|
|
319
|
+
flow.reset();
|
|
320
|
+
debug.info("stabilize loop: %s unstable, %s triggered", me._unstable.length, me._triggeredCastCount);
|
|
321
|
+
flow.then(me._loopTF);
|
|
322
|
+
// me.cfg.autoMount &&
|
|
323
|
+
(me._triggeredCastCount || me._unstable.length) && flow.run();
|
|
324
|
+
});
|
|
325
|
+
if ( !me._unstable.length && !me._triggeredCastCount ) {
|
|
326
|
+
// quiescent boundary. A rollback requested mid-stabilize supersedes everything —
|
|
327
|
+
// it re-mounts an earlier rev (and its concept-lib), so queued structural edits
|
|
328
|
+
// (issued after that rev) are discarded (#11.c.4).
|
|
329
|
+
if ( me._pendingRollback != null ) {
|
|
330
|
+
var _r = me._pendingRollback;
|
|
331
|
+
me._pendingRollback = null;
|
|
332
|
+
me._pendingStructural = [];
|
|
333
|
+
me._doRollback(_r);// mount + restore, no kick — the re-arm re-stabilizes
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
// else apply any structural ops (add/patchConcept) issued mid-stabilize, against
|
|
337
|
+
// the now-settled, consistent cast-state — then let the loop re-run (they
|
|
338
|
+
// write/destabilize) rather than declaring stable (#11.a).
|
|
339
|
+
if ( me._pendingStructural && me._pendingStructural.length ) {
|
|
340
|
+
me._drainStructural();
|
|
341
|
+
// A drain that changed cast-state destabilized objects -> the scheduled loop
|
|
342
|
+
// iteration runs the flow and re-enters here, re-checking quiescence. But a
|
|
343
|
+
// NO-OP drain (the patched concept's re-eval changes no cast — e.g. a tightening
|
|
344
|
+
// that excludes an object already un-cast for another reason) leaves the graph
|
|
345
|
+
// quiescent with nothing to run: the loop never re-arms and `_stabilizing` stays
|
|
346
|
+
// stuck true, so the `stabilize` event / a waiting ingest cb never fire. Only
|
|
347
|
+
// defer when the drain created work; otherwise fall through and settle.
|
|
348
|
+
if ( me._unstable.length || me._triggeredCastCount ) return;
|
|
349
|
+
}
|
|
350
|
+
debug.log("stabilize loop quiescent — settling");
|
|
351
|
+
me._applyStabilized();
|
|
352
|
+
}
|
|
353
|
+
},
|
|
354
|
+
printStats: function () {
|
|
355
|
+
var debug = this._log;
|
|
356
|
+
let stats = this._statsByProvider;
|
|
357
|
+
if ( !stats )
|
|
358
|
+
return;
|
|
359
|
+
let total = 0,
|
|
360
|
+
parts = {},
|
|
361
|
+
results = Object.keys(stats)
|
|
362
|
+
.sort(( a, b ) => (stats[b] - stats[a]))
|
|
363
|
+
.map(( a ) => {
|
|
364
|
+
total += stats[a];
|
|
365
|
+
return a
|
|
366
|
+
})
|
|
367
|
+
.map(( a, i ) => {
|
|
368
|
+
let insec = stats[a] / 1000,
|
|
369
|
+
pct = stats[a] * 100 / total;
|
|
370
|
+
pct = Math.round(pct * 1000) / 1000;
|
|
371
|
+
insec = Math.round(insec * 10) / 10;
|
|
372
|
+
return "\t" + (pct) + "%\t" + "( ~ " + (insec) + "s )\t:\t" + a;
|
|
373
|
+
}).join("\n");
|
|
374
|
+
debug.warn(
|
|
375
|
+
"____________________________________________________")
|
|
376
|
+
debug.warn(
|
|
377
|
+
"%s : Graph providers outer-stats total execTm [ %d s ] \n", this.cfg.label, ~~(total / 1000), results);
|
|
378
|
+
debug.warn(
|
|
379
|
+
"____________________________________________________")
|
|
380
|
+
},
|
|
381
|
+
/**
|
|
382
|
+
* get a serialized json copy of the graph
|
|
383
|
+
* @returns {{spatialEP: (*|string), servicesEP: *, timeStepEP: *, lastSpecified: (*|string), conceptMaps: Array}}
|
|
384
|
+
*/
|
|
385
|
+
serialize: function () {
|
|
386
|
+
var state = this._lastSyncState,
|
|
387
|
+
map = this._objById;
|
|
388
|
+
return {
|
|
389
|
+
...this._lastSyncRecord,
|
|
390
|
+
lastRev: this.getCurrentRevision(),
|
|
391
|
+
graph : JSON.stringify(
|
|
392
|
+
{
|
|
393
|
+
spatialEP : state.spatialEP,
|
|
394
|
+
lastRev : this.getCurrentRevision(),
|
|
395
|
+
conceptMaps: Object.keys(this._objById).map(
|
|
396
|
+
function ( id ) {
|
|
397
|
+
return { ...map[id]._etty._ };
|
|
398
|
+
}
|
|
399
|
+
),
|
|
400
|
+
bagRefs : Object.keys(this._bagRefsByRefId).reduce(
|
|
401
|
+
( r, id ) => {
|
|
402
|
+
r[id] = {
|
|
403
|
+
count: this._bagRefsByRefId[id].count
|
|
404
|
+
}
|
|
405
|
+
return r;
|
|
406
|
+
}, {}
|
|
407
|
+
),
|
|
408
|
+
|
|
409
|
+
}
|
|
410
|
+
)
|
|
411
|
+
}
|
|
412
|
+
},
|
|
413
|
+
/**
|
|
414
|
+
* Do mount the graph (instantiate all objects & mark them as unstable)
|
|
415
|
+
* @param sg serialized graph
|
|
416
|
+
*/
|
|
417
|
+
mount : function ( sg, cfg ) {
|
|
418
|
+
var debug = this._log;
|
|
419
|
+
var me = this, stack = [];
|
|
420
|
+
|
|
421
|
+
// copy original state
|
|
422
|
+
|
|
423
|
+
this._lastSyncState = sg = { ...sg };
|
|
424
|
+
this._triggeredCast = {};
|
|
425
|
+
// clean up / init ...
|
|
426
|
+
// user & datas open request
|
|
427
|
+
this._userQuery = [];
|
|
428
|
+
this._dataQuery = [];
|
|
429
|
+
|
|
430
|
+
this._rev = sg.lastRev || 1;
|
|
431
|
+
this._history = [];
|
|
432
|
+
this._unstable = [];
|
|
433
|
+
this._pending = [];
|
|
434
|
+
this._stable = [];
|
|
435
|
+
this._objById = {};
|
|
436
|
+
this._pendingMutationsById = {};
|
|
437
|
+
this.refMap = {};// per-instance (was declared on Graph.prototype → shared across every Graph in the process + grew unbounded)
|
|
438
|
+
sg.freeNodes = sg.freeNodes || [];
|
|
439
|
+
sg.nodes = sg.nodes || [];
|
|
440
|
+
sg.segments = sg.segments || [];
|
|
441
|
+
if ( sg.conceptMaps ) {
|
|
442
|
+
sg.conceptMaps.map(
|
|
443
|
+
function ( map ) {
|
|
444
|
+
if ( !map ) return debug.warn(sg.conceptMaps);
|
|
445
|
+
if ( map.Node ) sg.nodes.push(map);
|
|
446
|
+
else if ( map.Segment ) sg.segments.push(map);
|
|
447
|
+
else sg.freeNodes.push(map);
|
|
448
|
+
}
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
this._freeNodes = sg.freeNodes// free nodes are concept map / scope, linkable on node's & segment's conceptMap, on which we can cast
|
|
453
|
+
// concepts,
|
|
454
|
+
&& sg.freeNodes.map(function ( v ) {
|
|
455
|
+
me._objById[v._id] = { _etty: new Entity(v, me) };
|
|
456
|
+
me._unstable.push(me._objById[v._id]) || me._stable.push(me._objById[v._id]);
|
|
457
|
+
me._objById[v._id]._etty.updateApplicableConcepts(me);
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
return me._objById[v._id];
|
|
461
|
+
})
|
|
462
|
+
|| [];
|
|
463
|
+
this._nodes = sg.nodes// nodes first as segments autoregister them in nodes
|
|
464
|
+
&& sg.nodes.map(function ( v ) {
|
|
465
|
+
me._objById[v._id] = new Node(v, me);
|
|
466
|
+
// me.cfg.autoMount &&
|
|
467
|
+
me._unstable.push(me._objById[v._id]) || me._stable.push(me._objById[v._id]);
|
|
468
|
+
me._objById[v._id]._etty.updateApplicableConcepts(me);
|
|
469
|
+
|
|
470
|
+
return me._objById[v._id];
|
|
471
|
+
})
|
|
472
|
+
|| [];
|
|
473
|
+
this._segments = sg.segments
|
|
474
|
+
&& sg.segments.map(function ( v ) {
|
|
475
|
+
me._objById[v._id] = new Segment(v, me);
|
|
476
|
+
// me.cfg.autoMount &&
|
|
477
|
+
me._unstable.push(me._objById[v._id]) || me._stable.push(me._objById[v._id]);
|
|
478
|
+
me._objById[v._id]._etty.updateApplicableConcepts(me);
|
|
479
|
+
return me._objById[v._id];
|
|
480
|
+
})
|
|
481
|
+
|| [];
|
|
482
|
+
debug.verbose("graph mounted (%s objects)", Object.keys(this._objById || {}).length);
|
|
483
|
+
|
|
484
|
+
},
|
|
485
|
+
// refId -> [walked ref ids], so a multi-hop walk can tear down its old watchers
|
|
486
|
+
// when an intermediate ref is repointed (see the `refs[exp.length]` handling below).
|
|
487
|
+
refMap: {},
|
|
488
|
+
/**
|
|
489
|
+
* Resolve a reference expression against a scope object — the read half of the
|
|
490
|
+
* `$`-ref mini-DSL (the same syntax used in mutation templates and assert/query
|
|
491
|
+
* strings). Optionally installs a watcher so the scope is retested when the
|
|
492
|
+
* resolved value later changes/appears.
|
|
493
|
+
*
|
|
494
|
+
* The expression is a chain of two kinds of step:
|
|
495
|
+
* `.` walk INTO a key of the current object (Position.lat)
|
|
496
|
+
* `:` follow a reference to ANOTHER object, then keep walking
|
|
497
|
+
* (originNode:Position.lat → hop to the origin node, read its Position.lat)
|
|
498
|
+
* `$` prefix = a GLOBAL lookup by id in _objById ($paris, $paris:Position)
|
|
499
|
+
* A trailing `:` (e.g. "originNode:") returns the linked OBJECT itself, not a value.
|
|
500
|
+
* A bare key ("Distance") returns that fact off the scope.
|
|
501
|
+
*
|
|
502
|
+
* `follow`:
|
|
503
|
+
* - `true` → if the value is absent, destabilize `scope`'s object so it is
|
|
504
|
+
* retested once the value appears (this is how `require` waits on a
|
|
505
|
+
* not-yet-produced fact); internally normalized to the scope id.
|
|
506
|
+
* - a fn → call it on change instead of destabilizing.
|
|
507
|
+
* - falsy → plain read, no watcher.
|
|
508
|
+
* `unref` removes a previously-installed watcher instead of adding one.
|
|
509
|
+
* `getBox` resolves an id → Entity (defaults to this.getEtty; overridable for tests).
|
|
510
|
+
*
|
|
511
|
+
* bagRefs (external data ids matching cfg.bagRefManagers) resolve to the preloaded
|
|
512
|
+
* record in _bagRefsByRefId rather than to a graph object.
|
|
513
|
+
*
|
|
514
|
+
* @param {string} exp ref expression (e.g. "originNode:Position.lat")
|
|
515
|
+
* @param {Entity|string} scope object (or its id) the expression is relative to
|
|
516
|
+
* @param {boolean|fn} [follow] install a watcher (see above)
|
|
517
|
+
* @param {boolean} [unref] remove the watcher instead
|
|
518
|
+
* @param {fn} [getBox] id -> Entity resolver
|
|
519
|
+
* @returns the resolved value, or the linked Entity (trailing `:`), or undefined
|
|
520
|
+
*/
|
|
521
|
+
getRef: function ( exp, scope, follow, unref, getBox ) {
|
|
522
|
+
var debug = this._log;
|
|
523
|
+
let cScope = isString(scope) && this.getEtty(scope) || scope || this.getEtty(this.cfg.defaultContext),
|
|
524
|
+
refId = cScope && cScope._ && (cScope._._id + '::' + exp),
|
|
525
|
+
keyRefId,
|
|
526
|
+
refStack;
|
|
527
|
+
|
|
528
|
+
getBox = getBox || this.getEtty.bind(this);
|
|
529
|
+
|
|
530
|
+
exp = exp.split('.');
|
|
531
|
+
follow = follow === true && scope._._id || follow;
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
let refs = this.refMap[refId] = follow && !unref && this.refMap[refId] || [], bagRef;
|
|
535
|
+
|
|
536
|
+
while ( exp.length ) {
|
|
537
|
+
if ( exp[0].indexOf(':') != -1 ) {// follow the ref
|
|
538
|
+
exp[0] = exp[0].split(':');
|
|
539
|
+
|
|
540
|
+
while ( exp[0].length != 1 ) {// consume each `:`-hop, switching scope to the linked object
|
|
541
|
+
|
|
542
|
+
if ( exp[0][0][0] == '$' ) {// global ref
|
|
543
|
+
cScope = getBox(exp[0][0].substr(1));// switch scope
|
|
544
|
+
exp[0].shift();
|
|
545
|
+
}
|
|
546
|
+
else {
|
|
547
|
+
// read the id stored under this key, then hop to the object it names
|
|
548
|
+
keyRefId = cScope.get && cScope.get(exp[0][0], follow, unref) || cScope[exp[0][0]];// walk
|
|
549
|
+
bagRef = this._isBagRefs(keyRefId)
|
|
550
|
+
|
|
551
|
+
if ( bagRef ) // the id points at external (bagRef) data, not a graph object
|
|
552
|
+
{
|
|
553
|
+
if ( !this._bagRefsByRefId[keyRefId] )
|
|
554
|
+
debug.warn("ref to an unknown bagRef %s", keyRefId, cScope)
|
|
555
|
+
cScope = this._bagRefsByRefId[keyRefId] && this._bagRefsByRefId[keyRefId].record || null;
|
|
556
|
+
}
|
|
557
|
+
else {
|
|
558
|
+
// if this hop was repointed since last time, drop the stale watcher
|
|
559
|
+
// installed on the previous target's key before following the new one
|
|
560
|
+
if ( follow && !unref && refs[exp.length] && (refs[exp.length] != keyRefId) )
|
|
561
|
+
getBox(refs[exp.length]) &&
|
|
562
|
+
getBox(refs[exp.length])._etty.get(exp[0][0], follow, true);
|
|
563
|
+
|
|
564
|
+
cScope = getBox(keyRefId);// walk to the linked object
|
|
565
|
+
}
|
|
566
|
+
exp[0].shift();
|
|
567
|
+
|
|
568
|
+
}
|
|
569
|
+
if ( !cScope ) return;
|
|
570
|
+
}
|
|
571
|
+
if ( !exp[0][0] ) {
|
|
572
|
+
return cScope;// exp finishig by : ex: "originNode:"
|
|
573
|
+
}
|
|
574
|
+
exp[0] = exp[0][0];
|
|
575
|
+
}
|
|
576
|
+
if ( exp[0][0] == '$' ) {// global ref
|
|
577
|
+
if ( exp.length == 1 )
|
|
578
|
+
return this._objById[exp[0].substr(1)] && this._objById[exp[0].substr(1)]._etty._._id;
|
|
579
|
+
cScope = this._objById[exp[0].substr(1)] && this._objById[exp[0].substr(1)]._etty;
|
|
580
|
+
}
|
|
581
|
+
else if ( exp.length == 1 ) {
|
|
582
|
+
return cScope.get ? cScope.get(exp[0], follow, unref) : cScope[exp[0]];
|
|
583
|
+
|
|
584
|
+
}
|
|
585
|
+
else if ( exp.length ) {
|
|
586
|
+
cScope = cScope.get ? cScope.get(exp[0], follow, unref) : cScope[exp[0]];
|
|
587
|
+
}
|
|
588
|
+
if ( !cScope ) return;
|
|
589
|
+
exp.shift();
|
|
590
|
+
}
|
|
591
|
+
return cScope;
|
|
592
|
+
},
|
|
593
|
+
|
|
594
|
+
update: function ( record ) {
|
|
595
|
+
var debug = this._log;
|
|
596
|
+
var cRecord = this._lastSyncRecord,
|
|
597
|
+
changes = Object.keys(record).map(( b ) => ((record[b] != cRecord[b]) && b)).filter(
|
|
598
|
+
i => !!i || ["graph", "updated"].includes(i)
|
|
599
|
+
);
|
|
600
|
+
|
|
601
|
+
if ( !changes.length || (changes.length == 1) && changes[0] == "updated" ) {
|
|
602
|
+
|
|
603
|
+
return;// no changes
|
|
604
|
+
}
|
|
605
|
+
else {
|
|
606
|
+
debug.error(changes);
|
|
607
|
+
if ( this._rev <= record.lastRev ) {
|
|
608
|
+
|
|
609
|
+
this._revs[this._rev] = {
|
|
610
|
+
id : shortid.generate(),
|
|
611
|
+
recordUpdate: record
|
|
612
|
+
};
|
|
613
|
+
record.lastRev++;
|
|
614
|
+
this._rev++;
|
|
615
|
+
// this.stabilize();
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
this._lastSyncRecord = {
|
|
621
|
+
...this._lastSyncRecord,
|
|
622
|
+
...record
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
},
|
|
626
|
+
|
|
627
|
+
// -------------------------------------------------------------------------- atomic stuff
|
|
628
|
+
|
|
629
|
+
getCurrentRevision: function () {
|
|
630
|
+
return this._rev;
|
|
631
|
+
},
|
|
632
|
+
/**
|
|
633
|
+
* get all atoms from from to to
|
|
634
|
+
* @param from
|
|
635
|
+
* @param to
|
|
636
|
+
* @returns {Array.<T>}
|
|
637
|
+
*/
|
|
638
|
+
getRevisionsRange: function ( from, to ) {
|
|
639
|
+
this._log.verbose("getRevisionsRange %s..%s", from, to);
|
|
640
|
+
return this._revs.slice(from, to);
|
|
641
|
+
},
|
|
642
|
+
|
|
643
|
+
|
|
644
|
+
_mutationThread : [],
|
|
645
|
+
_mutationThreadRunning: false,
|
|
646
|
+
_atomicThread : [],
|
|
647
|
+
_atomicThreadRunning : false,
|
|
648
|
+
/**
|
|
649
|
+
* Push atoms from remote or client
|
|
650
|
+
* @param from
|
|
651
|
+
* @param to
|
|
652
|
+
* @param atoms
|
|
653
|
+
* @param token
|
|
654
|
+
* @param resetRevs
|
|
655
|
+
*/
|
|
656
|
+
pushAtomicUpdates: function ( from, to, atoms, token, resetRevs ) {
|
|
657
|
+
// !__SERVER__ &&
|
|
658
|
+
var debug = this._log;
|
|
659
|
+
var me = this,
|
|
660
|
+
i = 0,
|
|
661
|
+
max = to - from,
|
|
662
|
+
allRefs = {};
|
|
663
|
+
//
|
|
664
|
+
//if ( this._atomicThreadRunning ) {
|
|
665
|
+
//
|
|
666
|
+
// return this._atomicThread.push([...arguments]);
|
|
667
|
+
//}
|
|
668
|
+
//this._atomicThreadRunning = true;
|
|
669
|
+
debug.warn('RT Update request:', from, to, token, !__SERVER__ && atoms);
|
|
670
|
+
//while ( i < max ) {
|
|
671
|
+
// atoms[i]
|
|
672
|
+
// && atoms[i].bagRefs
|
|
673
|
+
// && atoms[i].bagRefs.length
|
|
674
|
+
// && atoms[i].bagRefs.each(id => {
|
|
675
|
+
// allRefs[id] = allRefs[id] || { count: 0 };
|
|
676
|
+
// allRefs[id].count++;
|
|
677
|
+
// });
|
|
678
|
+
// i++;
|
|
679
|
+
//}
|
|
680
|
+
//this._preloadBagRefs(
|
|
681
|
+
// allRefs,
|
|
682
|
+
// () => {
|
|
683
|
+
// me._inited = true;
|
|
684
|
+
while ( i < max ) {
|
|
685
|
+
atoms[i]
|
|
686
|
+
//&& !this._revByIds[atoms[i].id]//@todo: alpha method...
|
|
687
|
+
&& this.pushMutation(atoms[i].tpl, atoms[i].parent, true, atoms[i].id, atoms[i].bagRefs), i++;
|
|
688
|
+
}
|
|
689
|
+
token = isArray(token) ? token : token && [token] || [];
|
|
690
|
+
// token && this.on("stabilize", function fn() {// sync cb
|
|
691
|
+
// me.un("stabilize", fn);
|
|
692
|
+
//
|
|
693
|
+
// });
|
|
694
|
+
// if ( this.cfg.autoMount ) {
|
|
695
|
+
if ( !this._taskFlow.running ) {
|
|
696
|
+
this._taskFlow.run();
|
|
697
|
+
}
|
|
698
|
+
me._running = true;
|
|
699
|
+
token.map(( t ) => me._syncTokensList.push(t));
|
|
700
|
+
// } else this._applyStabilized();
|
|
701
|
+
|
|
702
|
+
token.map(( t ) => me._syncTokens[t] && me._syncTokens[t]());
|
|
703
|
+
this.stabilize(
|
|
704
|
+
//r=>{
|
|
705
|
+
//
|
|
706
|
+
//}
|
|
707
|
+
);
|
|
708
|
+
|
|
709
|
+
this._on.atomicUpdate
|
|
710
|
+
&& this._on.atomicUpdate.map(( cb ) => cb(me, from, to, atoms));
|
|
711
|
+
this.cfg.onAtomicUpdate
|
|
712
|
+
&& this.cfg.onAtomicUpdate(this, from, to, atoms);
|
|
713
|
+
//this._atomicThreadRunning = false;
|
|
714
|
+
//
|
|
715
|
+
//if ( this._atomicThread.length ) {
|
|
716
|
+
// this.pushAtomicUpdates(...this._atomicThread.shift())
|
|
717
|
+
//}
|
|
718
|
+
//}
|
|
719
|
+
//)
|
|
720
|
+
|
|
721
|
+
},
|
|
722
|
+
|
|
723
|
+
// -------------------------------------------------------------------------- mutations stuff
|
|
724
|
+
|
|
725
|
+
/**
|
|
726
|
+
* CDC ingestion — push live external-record field changes IN, through the SEQUENCED mutation path.
|
|
727
|
+
*
|
|
728
|
+
* This is the "revive the live-record path" entry point (study §5/B1, B9). The engine's bagRef binding
|
|
729
|
+
* is a read-once SNAPSHOT — correct for a record's STRUCTURAL fields (the decomposition keys on them;
|
|
730
|
+
* they must not churn mid-case). A record's DEFEASIBLE / live fields instead flow in here as ordinary
|
|
731
|
+
* GRAPH FACTS, set on the bound object, so the JTMS re-derives exactly the leaves that designed-in
|
|
732
|
+
* depend on them (a freshness `ensure` retracts a stale leaf; an `ensure` whose value flipped re-casts).
|
|
733
|
+
* That is the B9 structural-snapshot / defeasible-live split, and — crucially — it stays DETERMINISTIC:
|
|
734
|
+
* every change is a mutation TEMPLATE applied through `pushMutation` (taskflow-sequenced, rev-logged, so
|
|
735
|
+
* bisectable/replayable via `pushAtomicUpdates`), never an out-of-band `set`. This mirrors how the
|
|
736
|
+
* original host (epikeo) flowed external data: through the atomic-update / revision path, not by mutating
|
|
737
|
+
* the bagRef record in place.
|
|
738
|
+
*
|
|
739
|
+
* The whole batch is ONE sequenced settle (one rev step), not N — so a multi-record CDC tick is atomic.
|
|
740
|
+
*
|
|
741
|
+
* @param {object|Array} updates either `{ objId: { field: val, ... }, ... }`
|
|
742
|
+
* or `[ { id, fields:{...} }, ... ]`
|
|
743
|
+
* @param {fn} [cb] called once the ingest has settled
|
|
744
|
+
* @returns {Graph} this
|
|
745
|
+
*/
|
|
746
|
+
ingest: function ( updates, cb ) {
|
|
747
|
+
var tpl = [];
|
|
748
|
+
if ( isArray(updates) )
|
|
749
|
+
updates.forEach(function ( u ) { u && u.id != null && tpl.push({ ...(u.fields || {}), $$_id: u.id }); });
|
|
750
|
+
else if ( updates )
|
|
751
|
+
Object.keys(updates).forEach(function ( id ) { tpl.push({ ...updates[id], $$_id: id }); });
|
|
752
|
+
if ( !tpl.length ) { cb && setTimeout(cb); return this; }
|
|
753
|
+
this.pushMutation(tpl, null); // one sequenced, rev-logged mutation for the whole batch
|
|
754
|
+
this.stabilize(cb);
|
|
755
|
+
return this;
|
|
756
|
+
},
|
|
757
|
+
/**
|
|
758
|
+
*
|
|
759
|
+
* @param path
|
|
760
|
+
* @param mapLib
|
|
761
|
+
* @param tSegment
|
|
762
|
+
*/
|
|
763
|
+
pushMutationFromPath: function ( path, descrs, tSegment ) {
|
|
764
|
+
this.pushMutation(this.getMutationFromPath(path, descrs, tSegment), tSegment)
|
|
765
|
+
},
|
|
766
|
+
/**
|
|
767
|
+
* convert a path to a graph template
|
|
768
|
+
* @param path
|
|
769
|
+
* @param mapLib
|
|
770
|
+
* @param tSegment
|
|
771
|
+
* @returns {Array}
|
|
772
|
+
*/
|
|
773
|
+
getMutationFromPath: function ( path, descrs, tSegment ) {
|
|
774
|
+
var
|
|
775
|
+
me = this,
|
|
776
|
+
tpl = path.slice(0);
|
|
777
|
+
tpl.unshift.apply(tpl, descrs);//?
|
|
778
|
+
tpl = tpl.map(
|
|
779
|
+
function ( cmap ) {
|
|
780
|
+
cmap = { ...cmap };
|
|
781
|
+
if ( cmap.__bagRefKeys ) {
|
|
782
|
+
cmap.__bagRefKeys.forEach(
|
|
783
|
+
key => {
|
|
784
|
+
if ( cmap[key] )
|
|
785
|
+
cmap['$$' + key] = cmap[key];
|
|
786
|
+
delete cmap[key];
|
|
787
|
+
}
|
|
788
|
+
)
|
|
789
|
+
delete cmap.__bagRefKeys;
|
|
790
|
+
}
|
|
791
|
+
// if ( cmap.TimePeriod ) {// periods from the vendor record
|
|
792
|
+
// Period.start = Math.min(Period.start, cmap.TimePeriod.start);
|
|
793
|
+
// Period.end = Math.max(Period.end, cmap.TimePeriod.end);
|
|
794
|
+
// }
|
|
795
|
+
// if ( (related = cmap.pathDescriptor) && mapLib[related] ) {
|
|
796
|
+
// do {
|
|
797
|
+
// if ( !relatedRefs[related] ) {
|
|
798
|
+
// relatedRefs[related] = merge(true, mapLib[related]);
|
|
799
|
+
// relatedTpl.push(relatedRefs[related]);
|
|
800
|
+
// }
|
|
801
|
+
// } while (related = mapLib[related] && mapLib[related].parentPathDescriptor)
|
|
802
|
+
// }
|
|
803
|
+
if ( cmap._rev )
|
|
804
|
+
delete cmap._rev;
|
|
805
|
+
// if ( me._objById[cmap._id] ) {// existing turn to ref
|
|
806
|
+
//
|
|
807
|
+
// // if (cmap._id==)
|
|
808
|
+
//
|
|
809
|
+
// cmap.$_id = '$' + cmap._id + '._id';
|
|
810
|
+
// delete cmap._id;
|
|
811
|
+
//
|
|
812
|
+
// }
|
|
813
|
+
return cmap;
|
|
814
|
+
}
|
|
815
|
+
);
|
|
816
|
+
return tpl;
|
|
817
|
+
},
|
|
818
|
+
/**
|
|
819
|
+
* Manual/forced concept uncast
|
|
820
|
+
* @param cmapId the target cmap id
|
|
821
|
+
* @param cId the concept id
|
|
822
|
+
* @param cb
|
|
823
|
+
*/
|
|
824
|
+
unCastConcept: function ( cmapId, cId, cb ) {
|
|
825
|
+
if ( this._objById[cmapId]._etty._._autokill ) return;
|
|
826
|
+
var me = this, key = cmapId + '/' + cId;
|
|
827
|
+
if ( this._triggeredCast[key] )
|
|
828
|
+
delete this._triggeredCast[key];
|
|
829
|
+
this.pushMutation(
|
|
830
|
+
{
|
|
831
|
+
$_id : "_parent",
|
|
832
|
+
[this._conceptLib[cId]._name]: null
|
|
833
|
+
},
|
|
834
|
+
cmapId
|
|
835
|
+
);
|
|
836
|
+
// this._objById[cmapId]._etty.unCast(this._conceptLib[cId]._name, null);
|
|
837
|
+
// this.toggleGraphObjectState(cmapId, "unstable");
|
|
838
|
+
this.stabilize(cb);
|
|
839
|
+
},
|
|
840
|
+
/**
|
|
841
|
+
* Manual/forced concept cast
|
|
842
|
+
* @param cmapId the target cmap id
|
|
843
|
+
* @param cId the concept id
|
|
844
|
+
* @param cb
|
|
845
|
+
*/
|
|
846
|
+
castConcept : function ( cmapId, cId, cb ) {
|
|
847
|
+
if ( this._objById[cmapId]._etty._._autokill ) return;
|
|
848
|
+
var me = this, key = cmapId + '/' + cId;
|
|
849
|
+
// this._triggeredCast[key] = [cmapId, cId];
|
|
850
|
+
// me._triggeredCastCount++;
|
|
851
|
+
this._taskFlow.pushSubTask(
|
|
852
|
+
this._conceptLib[cId].applyTo(this._objById[cmapId]._etty, this));
|
|
853
|
+
this.toggleGraphObjectState(cmapId, "unstable");
|
|
854
|
+
this.stabilize(cb);
|
|
855
|
+
},
|
|
856
|
+
/**
|
|
857
|
+
* Resolve a concept by its id (`_conceptLib` key) or, failing that, by its
|
|
858
|
+
* `_name`. Returns the Concept instance or null.
|
|
859
|
+
* @param nameOrId
|
|
860
|
+
*/
|
|
861
|
+
getConceptByName: function ( nameOrId ) {
|
|
862
|
+
if ( this._conceptLib[nameOrId] ) return this._conceptLib[nameOrId];
|
|
863
|
+
var ids = Object.keys(this._conceptLib);
|
|
864
|
+
for ( var i = 0; i < ids.length; i++ )
|
|
865
|
+
if ( this._conceptLib[ids[i]]._name === nameOrId )
|
|
866
|
+
return this._conceptLib[ids[i]];
|
|
867
|
+
return null;
|
|
868
|
+
},
|
|
869
|
+
/**
|
|
870
|
+
* Hot-patch an expert (concept) and re-evaluate the WHOLE graph against it,
|
|
871
|
+
* in both directions — no restart, no rebuild:
|
|
872
|
+
* - patch the concept's schema + recompile its applicability test
|
|
873
|
+
* (delegated to Concept.patch);
|
|
874
|
+
* - for every live object: newly-applicable + not cast -> castConcept;
|
|
875
|
+
* cast + no-longer-applicable -> unCast (which cascades to children);
|
|
876
|
+
* - re-stabilize.
|
|
877
|
+
*
|
|
878
|
+
* @param nameOrId concept id (`_conceptLib` key) or `_name`
|
|
879
|
+
* @param updates partial concept schema (e.g. `{ assert: ["$x > 5"] }`)
|
|
880
|
+
* @param cb optional stabilize callback
|
|
881
|
+
* @returns {Graph} this
|
|
882
|
+
*/
|
|
883
|
+
patchConcept : function ( nameOrId, updates, cb ) {
|
|
884
|
+
// Issued mid-stabilize? Defer to the quiescent _loopTF boundary so the re-eval
|
|
885
|
+
// sees a settled cast-state (else a patch of the concept currently mid-apply is
|
|
886
|
+
// silently dropped — its self-flag is not written yet). (#11.a re-entrancy.)
|
|
887
|
+
if ( this._stabilizing ) {
|
|
888
|
+
this._pendingStructural.push({ op: 'patch', nameOrId: nameOrId, updates: updates, cb: cb });
|
|
889
|
+
return this;
|
|
890
|
+
}
|
|
891
|
+
this._doPatchConcept(nameOrId, updates);
|
|
892
|
+
this.stabilize(cb);
|
|
893
|
+
return this;
|
|
894
|
+
},
|
|
895
|
+
/**
|
|
896
|
+
* The bidirectional re-eval body of patchConcept WITHOUT the stabilize kick.
|
|
897
|
+
* Casts (no-kick: queue the apply + destabilize) newly-applicable objects and
|
|
898
|
+
* uncasts (cascades) no-longer-applicable ones. Used by the public method (host
|
|
899
|
+
* path, one kick at the end) and by the mid-stabilize drain.
|
|
900
|
+
*/
|
|
901
|
+
_doPatchConcept: function ( nameOrId, updates ) {
|
|
902
|
+
var me = this,
|
|
903
|
+
concept = this.getConceptByName(nameOrId);
|
|
904
|
+
if ( !concept )
|
|
905
|
+
throw new Error("patchConcept: no concept '" + nameOrId + "'");
|
|
906
|
+
|
|
907
|
+
concept.patch(updates);
|
|
908
|
+
this._conceptSnapshot = null;// concept lib changed — invalidate the N6 schema snapshot cache
|
|
909
|
+
|
|
910
|
+
// #11.b scoped re-eval: only the objects whose cast-state for C could change —
|
|
911
|
+
// not the whole graph (was O(graph) stop-the-world).
|
|
912
|
+
this._scopedReevalIds(concept).forEach(function ( id ) {
|
|
913
|
+
var o = me._objById[id];
|
|
914
|
+
if ( !o ) return;
|
|
915
|
+
var etty = o._etty;
|
|
916
|
+
if ( !etty || etty._dead ) return;
|
|
917
|
+
var applicable = !!concept.isApplicableTo(etty, me),
|
|
918
|
+
isCast = !!etty._mappedConcepts[concept._name];
|
|
919
|
+
if ( applicable && !isCast ) {
|
|
920
|
+
me._taskFlow.pushSubTask(concept.applyTo(etty, me));// no-kick cast
|
|
921
|
+
me.toggleGraphObjectState(id, "unstable");
|
|
922
|
+
}
|
|
923
|
+
else if ( !applicable && isCast ) {
|
|
924
|
+
etty.unCast(concept._name);
|
|
925
|
+
me.toggleGraphObjectState(id, "unstable");
|
|
926
|
+
}
|
|
927
|
+
});
|
|
928
|
+
return concept;
|
|
929
|
+
},
|
|
930
|
+
/**
|
|
931
|
+
* The objects whose cast-state for `concept` could change under a patch — its
|
|
932
|
+
* sound, minimal re-eval frontier (#11.b):
|
|
933
|
+
* - `_mapsByConcept[C._name]` — objects where C is/was cast (the UNCAST direction
|
|
934
|
+
* on a tightening patch);
|
|
935
|
+
* - `_mapsByConcept[r]` for each simple `require` r — objects that carry a fact C
|
|
936
|
+
* needs, so a loosening patch could newly CAST C on them (incl. never-cast ones).
|
|
937
|
+
* Falls back to the full object scan when it can't be soundly scoped: an assert-only
|
|
938
|
+
* concept (no `require` ⇒ any object could apply) or a cross-object walk require
|
|
939
|
+
* (`a:b` ⇒ the require fact lives on another object, not indexable here).
|
|
940
|
+
*/
|
|
941
|
+
_scopedReevalIds: function ( concept ) {
|
|
942
|
+
var me = this,
|
|
943
|
+
requires = isArray(concept._schema.require) ? concept._schema.require
|
|
944
|
+
: concept._schema.require && [concept._schema.require] || [];
|
|
945
|
+
if ( !requires.length || requires.some(function ( r ) { return String(r).indexOf(':') !== -1; }) )
|
|
946
|
+
return Object.keys(me._objById);
|
|
947
|
+
var set = {};
|
|
948
|
+
(me._mapsByConcept[concept._name] || []).forEach(function ( id ) { set[id] = 1; });
|
|
949
|
+
requires.forEach(function ( r ) {
|
|
950
|
+
(me._mapsByConcept[r] || []).forEach(function ( id ) { set[id] = 1; });
|
|
951
|
+
});
|
|
952
|
+
return Object.keys(set);
|
|
953
|
+
},
|
|
954
|
+
/**
|
|
955
|
+
* Apply-ceiling backstop (#11.c.1): a (target, concept) pair has applied past the
|
|
956
|
+
* `_applyCap` within one episode. NOTE the framing: a self-destabilizing re-cast loop
|
|
957
|
+
* is NOT inherently pathological — it is a legitimate *iterative-trial* technique
|
|
958
|
+
* (the engine's own way to stabilize / try paths & casts). So the cap is a BACKSTOP
|
|
959
|
+
* (default high, reset on each settle so a converging trial loop is never killed), and
|
|
960
|
+
* `divergent` is the "did not converge within the ceiling" *outcome* — a reusable trial
|
|
961
|
+
* signal, not just an error. (Future: an explicit per-concept iteration budget could
|
|
962
|
+
* turn this into a controlled explore-variations loop — AO-star/beam + the learning loop.)
|
|
963
|
+
*
|
|
964
|
+
* Record WHY — a reason record pushed (race-free `{__push}`) into the target's
|
|
965
|
+
* `divergent` array fact. That fact is a NON-CAST CONDITION (Concept.isApplicableTo
|
|
966
|
+
* reads it), so the concept de-casts and stops re-firing; it is also a retraction
|
|
967
|
+
* trigger a host / meta-concept can `ensure`-gate on. Idempotent (one record per pair).
|
|
968
|
+
*/
|
|
969
|
+
_markDivergent: function ( scope, concept, count ) {
|
|
970
|
+
var debug = this._log;
|
|
971
|
+
var cur = scope._ && scope._.divergent;
|
|
972
|
+
if ( cur && cur.length )
|
|
973
|
+
for ( var i = 0; i < cur.length; i++ )
|
|
974
|
+
if ( cur[i] && cur[i].concept === concept._name ) return;// already recorded
|
|
975
|
+
debug.error("DIVERGENT: %s blew the apply ceiling (%s) on %s — de-casting + flagging why",
|
|
976
|
+
concept._name, this._applyCap, scope._._id);
|
|
977
|
+
this.pushMutation({
|
|
978
|
+
$_id : "_parent",
|
|
979
|
+
divergent: { __push: { concept: concept._name, applies: count, cap: this._applyCap, reason: "apply-cap" } }
|
|
980
|
+
}, scope._._id);
|
|
981
|
+
},
|
|
982
|
+
/**
|
|
983
|
+
* Install a NEW expert (concept) into the live library and re-evaluate the
|
|
984
|
+
* graph against it — the symmetric twin of `patchConcept`, no restart/rebuild.
|
|
985
|
+
* This is the engine half of declarative AI-authoring (roadmap #10): an
|
|
986
|
+
* authoring loop proposes a concept term, the host validator gates it, this
|
|
987
|
+
* installs it.
|
|
988
|
+
*
|
|
989
|
+
* It composes existing machinery only (Concept ctor + the cast/sweep/stabilize
|
|
990
|
+
* path), so there is no new evaluation code:
|
|
991
|
+
* - `new Concept` registers itself (and any nested `childConcepts`) into
|
|
992
|
+
* `_conceptLib` keyed by `_id`, and recompiles its applicability test;
|
|
993
|
+
* - it is attached under `parent._openConcepts` keyed by `_id` (the engine
|
|
994
|
+
* invariant: a child's key in `_openConcepts`/`childConcepts` IS its `_id`,
|
|
995
|
+
* because `updateApplicableConcepts` pushes those keys straight into
|
|
996
|
+
* `_conceptLib[...]`), and mirrored into `parent._schema.childConcepts` so
|
|
997
|
+
* serialize() carries the new capability;
|
|
998
|
+
* - every live object under the (cast) parent has the new concept opened in
|
|
999
|
+
* its `_mapOpenConcepts` and is re-swept — exactly how an object discovers
|
|
1000
|
+
* a root concept at mount. Unresolved `require`s install their follow-watcher
|
|
1001
|
+
* during the sweep, so a dormant concept fires when its fact later appears.
|
|
1002
|
+
*
|
|
1003
|
+
* Re-entrancy note: like `patchConcept`, this casts/uncasts + `stabilize()`s
|
|
1004
|
+
* directly (not via the mutation queue). It is meant to be called from the host
|
|
1005
|
+
* at a quiescent boundary (between stabilizations). Calling it from *inside* a
|
|
1006
|
+
* provider mid-stabilize is the self-modification tier (roadmap #11) and needs
|
|
1007
|
+
* the queued/scoped variant (MODELISATION §6.4) — not done here.
|
|
1008
|
+
*
|
|
1009
|
+
* @param parentNameOrId parent concept id/`_name`; null/undefined = top-level
|
|
1010
|
+
* (child of the root container)
|
|
1011
|
+
* @param schema a concept schema: `{ _id, _name?, require?, assert?,
|
|
1012
|
+
* ensure?, provider?, applyMutations?, childConcepts? }`.
|
|
1013
|
+
* `_id` must be globally unique; `_name` defaults to `_id`.
|
|
1014
|
+
* @param cb optional stabilize callback
|
|
1015
|
+
* @returns {Concept} the installed concept
|
|
1016
|
+
*/
|
|
1017
|
+
addConcept : function ( parentNameOrId, schema, cb ) {
|
|
1018
|
+
// Issued mid-stabilize (e.g. from a meta-concept's provider)? Defer to the
|
|
1019
|
+
// quiescent _loopTF boundary, where cast-state is settled (#11.a re-entrancy).
|
|
1020
|
+
// The deferred op installs at drain time; its cb gets (err, concept) then.
|
|
1021
|
+
if ( this._stabilizing ) {
|
|
1022
|
+
this._pendingStructural.push({ op: 'add', parentNameOrId: parentNameOrId, schema: schema, cb: cb });
|
|
1023
|
+
return;
|
|
1024
|
+
}
|
|
1025
|
+
var concept = this._doAddConcept(parentNameOrId, schema);
|
|
1026
|
+
this.stabilize(cb);
|
|
1027
|
+
return concept;
|
|
1028
|
+
},
|
|
1029
|
+
/**
|
|
1030
|
+
* The structural body of addConcept WITHOUT the stabilize kick — builds+registers
|
|
1031
|
+
* the concept, attaches it to the parent, opens it on the live objects, and
|
|
1032
|
+
* destabilizes them. Used directly by the public method (host path) and by the
|
|
1033
|
+
* mid-stabilize drain (`_drainStructural`).
|
|
1034
|
+
*/
|
|
1035
|
+
_doAddConcept : function ( parentNameOrId, schema ) {
|
|
1036
|
+
var me = this;
|
|
1037
|
+
if ( !schema || !schema._id )
|
|
1038
|
+
throw new Error("addConcept: schema must have a unique _id");
|
|
1039
|
+
if ( this._conceptLib[schema._id] )
|
|
1040
|
+
throw new Error("addConcept: concept '" + schema._id + "' already exists (duplicate _id)");
|
|
1041
|
+
if ( !schema._name ) schema._name = schema._id;
|
|
1042
|
+
|
|
1043
|
+
var parent = parentNameOrId ? this.getConceptByName(parentNameOrId) : this._rootConcept;
|
|
1044
|
+
if ( !parent )
|
|
1045
|
+
throw new Error("addConcept: no parent concept '" + parentNameOrId + "'");
|
|
1046
|
+
|
|
1047
|
+
// build + register (Concept.init does graph._conceptLib[_id]=this, recursively
|
|
1048
|
+
// for nested childConcepts, and compiles the applicability test)
|
|
1049
|
+
var concept = new Concept(schema, this, parent === this._rootConcept ? undefined : parent);
|
|
1050
|
+
|
|
1051
|
+
// attach under the parent's open-concept map (key === _id, the engine invariant)
|
|
1052
|
+
if ( !parent._openConcepts ) {
|
|
1053
|
+
parent._openConcepts = {};
|
|
1054
|
+
parent._openConceptsRequires = {};
|
|
1055
|
+
parent.isLeaf = false;
|
|
1056
|
+
}
|
|
1057
|
+
parent._openConcepts[schema._id] = concept;
|
|
1058
|
+
parent._openConceptsRequires[schema._id] =
|
|
1059
|
+
isArray(schema.require) ? schema.require : schema.require && [schema.require] || [];
|
|
1060
|
+
// mirror into the parent's live schema so serialize()/snapshots carry it
|
|
1061
|
+
parent._schema.childConcepts = parent._schema.childConcepts || {};
|
|
1062
|
+
parent._schema.childConcepts[schema._id] = schema;
|
|
1063
|
+
this._conceptSnapshot = null;// concept lib changed — invalidate the N6 schema snapshot cache
|
|
1064
|
+
|
|
1065
|
+
// open + re-sweep every live object the new concept could apply to: the root
|
|
1066
|
+
// container reaches all objects; a real parent reaches objects it is cast on.
|
|
1067
|
+
var atRoot = parent === this._rootConcept;
|
|
1068
|
+
Object.keys(this._objById).forEach(function ( id ) {
|
|
1069
|
+
var etty = me._objById[id]._etty;
|
|
1070
|
+
if ( !etty || etty._dead ) return;
|
|
1071
|
+
if ( !atRoot && !etty._mappedConcepts[parent._name] ) return;
|
|
1072
|
+
if ( etty._mapOpenConcepts.indexOf(schema._id) === -1 )
|
|
1073
|
+
etty._mapOpenConcepts.push(schema._id);
|
|
1074
|
+
etty.updateApplicableConcepts(me);
|
|
1075
|
+
me.toggleGraphObjectState(id, "unstable");
|
|
1076
|
+
});
|
|
1077
|
+
|
|
1078
|
+
return concept;
|
|
1079
|
+
},
|
|
1080
|
+
/**
|
|
1081
|
+
* REMOVE an expert (concept) from the live library and re-stabilize — the symmetric
|
|
1082
|
+
* inverse of `addConcept`, completing the grammar CRUD (an operator editing a grammar
|
|
1083
|
+
* in the Studio must be able to delete a concept, not only add/patch it). Composes
|
|
1084
|
+
* existing machinery only (`Entity.unCast` + the sweep/stabilize path): no new teardown
|
|
1085
|
+
* code. Deleting a concept with children removes the whole SUBTREE (its descendants are
|
|
1086
|
+
* only reachable under it). Like add/patchConcept it is re-entrant — issued mid-stabilize
|
|
1087
|
+
* it defers to the quiescent `_loopTF` boundary (#11.a).
|
|
1088
|
+
*
|
|
1089
|
+
* @param nameOrId concept id (`_conceptLib` key) or `_name`
|
|
1090
|
+
* @param cb optional stabilize callback
|
|
1091
|
+
* @returns {Graph} this
|
|
1092
|
+
*/
|
|
1093
|
+
deleteConcept : function ( nameOrId, cb ) {
|
|
1094
|
+
if ( this._stabilizing ) {
|
|
1095
|
+
this._pendingStructural.push({ op: 'delete', nameOrId: nameOrId, cb: cb });
|
|
1096
|
+
return this;
|
|
1097
|
+
}
|
|
1098
|
+
this._doDeleteConcept(nameOrId);
|
|
1099
|
+
this.stabilize(cb);
|
|
1100
|
+
return this;
|
|
1101
|
+
},
|
|
1102
|
+
/** The concept ids of `concept` and all its descendants (its `_openConcepts` subtree). */
|
|
1103
|
+
_conceptSubtreeIds: function ( concept ) {
|
|
1104
|
+
var ids = [concept._id], oc = concept._openConcepts;
|
|
1105
|
+
if ( oc ) for ( var k in oc ) ids = ids.concat(this._conceptSubtreeIds(oc[k]));
|
|
1106
|
+
return ids;
|
|
1107
|
+
},
|
|
1108
|
+
/**
|
|
1109
|
+
* The structural body of deleteConcept WITHOUT the stabilize kick. UN-CASTS the concept
|
|
1110
|
+
* from every live object (permanent — `unCast(name, _id)` does NOT re-open it, and cascades
|
|
1111
|
+
* to its own CHILDREN), destabilizes the objects it was cast on, detaches it from its parent
|
|
1112
|
+
* (open map + `_schema.childConcepts`, so serialize()/snapshots drop it), and removes the
|
|
1113
|
+
* subtree from `_conceptLib` + the per-concept index. Used by the public method (host path)
|
|
1114
|
+
* and the mid-stabilize drain.
|
|
1115
|
+
*
|
|
1116
|
+
* v0 LIMIT (honest): an already-cast DEPENDENT — a sibling concept that `require`s the deleted
|
|
1117
|
+
* concept's fact — is NOT auto-un-cast (the engine's require-stickiness: a cast concept does not
|
|
1118
|
+
* re-evaluate on a require change, finding #22). The delete removes the TARGET (+ its subtree)
|
|
1119
|
+
* soundly; it will simply not cast on FUTURE objects (its require fact is gone). Cascading to
|
|
1120
|
+
* stale existing dependents would need an explicit dependent re-eval (the patchConcept frontier
|
|
1121
|
+
* pattern) — a follow-up, not done here.
|
|
1122
|
+
*/
|
|
1123
|
+
_doDeleteConcept: function ( nameOrId ) {
|
|
1124
|
+
var me = this,
|
|
1125
|
+
concept = this.getConceptByName(nameOrId);
|
|
1126
|
+
if ( !concept )
|
|
1127
|
+
throw new Error("deleteConcept: no concept '" + nameOrId + "'");
|
|
1128
|
+
var subtree = this._conceptSubtreeIds(concept);
|
|
1129
|
+
|
|
1130
|
+
// un-cast from EVERY live object: cast objects retract (+ destabilize so dependents re-eval),
|
|
1131
|
+
// objects where it is merely OPEN (dormant) drop it from their open set (the unReachable filter).
|
|
1132
|
+
Object.keys(this._objById).forEach(function ( id ) {
|
|
1133
|
+
var o = me._objById[id], etty = o && o._etty;
|
|
1134
|
+
if ( !etty || etty._dead ) return;
|
|
1135
|
+
var wasCast = !!etty._mappedConcepts[concept._name];
|
|
1136
|
+
etty.unCast(concept._name, concept._id);// _id ⇒ permanent (no re-open) + cascades to children
|
|
1137
|
+
if ( wasCast ) me.toggleGraphObjectState(id, "unstable");
|
|
1138
|
+
});
|
|
1139
|
+
|
|
1140
|
+
// detach from the parent so serialize()/snapshots no longer carry it
|
|
1141
|
+
var parent = concept._parent || this._rootConcept;
|
|
1142
|
+
if ( parent._openConcepts ) delete parent._openConcepts[concept._id];
|
|
1143
|
+
if ( parent._openConceptsRequires ) delete parent._openConceptsRequires[concept._id];
|
|
1144
|
+
if ( parent._schema && parent._schema.childConcepts ) delete parent._schema.childConcepts[concept._id];
|
|
1145
|
+
|
|
1146
|
+
// drop the subtree from the registry + the per-concept index
|
|
1147
|
+
subtree.forEach(function ( cid ) {
|
|
1148
|
+
var c = me._conceptLib[cid];
|
|
1149
|
+
if ( c && c._name ) delete me._mapsByConcept[c._name];
|
|
1150
|
+
delete me._conceptLib[cid];
|
|
1151
|
+
});
|
|
1152
|
+
this._conceptSnapshot = null;// concept lib changed — invalidate the N6 schema snapshot cache
|
|
1153
|
+
return concept;
|
|
1154
|
+
},
|
|
1155
|
+
/**
|
|
1156
|
+
* Drain structural ops (add/patch/deleteConcept) that were queued because they were
|
|
1157
|
+
* issued mid-stabilize. Runs at the quiescent _loopTF boundary, so the re-eval
|
|
1158
|
+
* sees a settled, consistent cast-state. Each op writes/destabilizes; _loopTF's
|
|
1159
|
+
* re-arm then re-stabilizes (no extra kick needed). (#11.a)
|
|
1160
|
+
*/
|
|
1161
|
+
_drainStructural: function () {
|
|
1162
|
+
var debug = this._log;
|
|
1163
|
+
var me = this, q = this._pendingStructural;
|
|
1164
|
+
this._pendingStructural = [];
|
|
1165
|
+
q.forEach(function ( o ) {
|
|
1166
|
+
try {
|
|
1167
|
+
var r = o.op === 'patch' ? me._doPatchConcept(o.nameOrId, o.updates)
|
|
1168
|
+
: o.op === 'delete' ? me._doDeleteConcept(o.nameOrId)
|
|
1169
|
+
: me._doAddConcept(o.parentNameOrId, o.schema);
|
|
1170
|
+
o.cb && o.cb(null, r);
|
|
1171
|
+
} catch ( e ) {
|
|
1172
|
+
debug.error("drainStructural %s failed: %s", o.op, e && e.message);
|
|
1173
|
+
o.cb && o.cb(e);
|
|
1174
|
+
}
|
|
1175
|
+
});
|
|
1176
|
+
},
|
|
1177
|
+
pushAtomicData: function ( data, revFrom, token ) {
|
|
1178
|
+
var debug = this._log;
|
|
1179
|
+
var me = this;
|
|
1180
|
+
debug.log("Start pushing from client %j", revFrom);
|
|
1181
|
+
token = isArray(token) ? token : token && [token] || [];
|
|
1182
|
+
|
|
1183
|
+
this.pushMutation(data.tpl, data.parent, true);
|
|
1184
|
+
token.map(( t ) => me._syncTokensList.push(t));
|
|
1185
|
+
// token && this.on("stabilize", function fn() {// sync cb
|
|
1186
|
+
// me.un("stabilize", fn);
|
|
1187
|
+
// token.map(( t )=>me._syncTokens[t] && me._syncTokens[t]());
|
|
1188
|
+
//
|
|
1189
|
+
// });
|
|
1190
|
+
},
|
|
1191
|
+
/**
|
|
1192
|
+
* Apply a mutation template — the one path that creates/updates graph objects.
|
|
1193
|
+
* Every touched object is destabilized so stabilization re-tests concepts on it.
|
|
1194
|
+
*
|
|
1195
|
+
* A template is an object (or array of objects) describing nodes/segments/docs.
|
|
1196
|
+
* Keys are facts; a few `$`-prefixed forms have meaning (the WRITE half of the
|
|
1197
|
+
* ref DSL — see getRef for the read half):
|
|
1198
|
+
* _id create a new object with a fresh id
|
|
1199
|
+
* $_id derive this object's id by RESOLVING a ref (e.g. "_parent" → targetId)
|
|
1200
|
+
* $$_id force a LITERAL id (upsert that exact object)
|
|
1201
|
+
* $key make `key` a REFERENCE (its value is resolved to an id)
|
|
1202
|
+
* $$key mark `key` a bagRef (external data id, preloaded before apply)
|
|
1203
|
+
* _incoming / _outgoing nest child segments under a node
|
|
1204
|
+
* { __push: v } race-free append to an array fact (see Entity.set)
|
|
1205
|
+
*
|
|
1206
|
+
* Pipeline:
|
|
1207
|
+
* 1. WALK the template (a stack), resolving `$`-refs against `refScope`/`aliases`
|
|
1208
|
+
* and collecting bagRefs to preload. Template-local ids are aliased to graph ids.
|
|
1209
|
+
* 2. PRELOAD any bagRefs (async), then in the callback:
|
|
1210
|
+
* 3. INSTANTIATE — nodes first, then segments/docs (so a segment's endpoints exist),
|
|
1211
|
+
* via Entity.update; record a revision (revTpl) for history/sync; fire the
|
|
1212
|
+
* onConceptApply trace if this came from a concept (applyCtx); destabilize +
|
|
1213
|
+
* stabilize().
|
|
1214
|
+
* Non-master graphs forward the template to the master (cfg.pushToMaster) and apply
|
|
1215
|
+
* the streamed-back result instead of mutating locally.
|
|
1216
|
+
*
|
|
1217
|
+
* @param {object|object[]} template the mutation template
|
|
1218
|
+
* @param {string} targetId object the template is applied to ("_parent")
|
|
1219
|
+
* @param {boolean} [force] bypass the master/client deferral
|
|
1220
|
+
* @param {*} [atomId] atomic-update id (master/client sync)
|
|
1221
|
+
* @param {object} [initialRefBag] preseeded bagRefs
|
|
1222
|
+
* @param {fn} [cb] called once applied (after bagRef preload)
|
|
1223
|
+
* @param {object} [applyCtx] concept-apply trace context (from Concept.applyTo)
|
|
1224
|
+
*/
|
|
1225
|
+
pushMutation: function ( template, targetId, force, atomId, initialRefBag, cb, applyCtx ) {
|
|
1226
|
+
template = isArray(template) ? template : [template];
|
|
1227
|
+
var debug = this._log;// per-graph logger (captured by nested callbacks below)
|
|
1228
|
+
var me = this,
|
|
1229
|
+
cObject, cTargetObj,
|
|
1230
|
+
push = Array.prototype.push,
|
|
1231
|
+
tid, refId, revId, revNum,
|
|
1232
|
+
cTplObject,
|
|
1233
|
+
revTpl = [],
|
|
1234
|
+
stack = [],
|
|
1235
|
+
pendingObjects = [],
|
|
1236
|
+
pendingRefs = [],
|
|
1237
|
+
pendingRefMaps = [],
|
|
1238
|
+
pendingERefs = [],
|
|
1239
|
+
refs = {},
|
|
1240
|
+
masterToken,
|
|
1241
|
+
keepRev = !this.cfg.isMaster,
|
|
1242
|
+
originCMap = targetId && me.getEtty(targetId) && me.getEtty(targetId)._ || false,
|
|
1243
|
+
baseOrigin = originCMap && originCMap._keepOrigin && originCMap._origin || false,
|
|
1244
|
+
aliases = ["_parent"],
|
|
1245
|
+
refScope = {
|
|
1246
|
+
_parent: targetId
|
|
1247
|
+
},
|
|
1248
|
+
bagRefs = initialRefBag ? { ...initialRefBag } : {};
|
|
1249
|
+
|
|
1250
|
+
let refMap = {};
|
|
1251
|
+
if ( !force && !this.cfg.isMaster ) {
|
|
1252
|
+
debug.warn("pushing 2 master", targetId);
|
|
1253
|
+
// if (!me.cfg.isMaster && !me._inited){
|
|
1254
|
+
// this._taskFlow.wait();
|
|
1255
|
+
//
|
|
1256
|
+
// return me.on("atomicUpdate", function atomicUpdate() {
|
|
1257
|
+
// me.un("atomicUpdate", atomicUpdate);
|
|
1258
|
+
// me._inited=true;
|
|
1259
|
+
// me.pushMutation.apply(me, arguments);
|
|
1260
|
+
// me._taskFlow.release();
|
|
1261
|
+
// });
|
|
1262
|
+
// }
|
|
1263
|
+
if ( !this._taskFlow.running ) {
|
|
1264
|
+
this._taskFlow.run();
|
|
1265
|
+
}
|
|
1266
|
+
this._running = true;
|
|
1267
|
+
me._taskFlow.wait();
|
|
1268
|
+
masterToken = this.cfg.pushToMaster(
|
|
1269
|
+
{
|
|
1270
|
+
baseRev: me._rev,
|
|
1271
|
+
parent : targetId,
|
|
1272
|
+
tpl : template
|
|
1273
|
+
}
|
|
1274
|
+
);
|
|
1275
|
+
|
|
1276
|
+
this._syncTokens[masterToken] = () => {// here the server should have applied this mutation & pushed back the resulting mutations
|
|
1277
|
+
me._taskFlow.release();
|
|
1278
|
+
debug.info('Complete %s !', masterToken);
|
|
1279
|
+
delete me._syncTokens[masterToken];
|
|
1280
|
+
cb && cb({ /* should have refscope here*/ })
|
|
1281
|
+
};
|
|
1282
|
+
debug.warn('RT Push request waiting ', masterToken);
|
|
1283
|
+
return;
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
if ( this._mutationThreadRunning ) {
|
|
1287
|
+
debug.warn('Delay mutation', this._mutationThread.length);
|
|
1288
|
+
|
|
1289
|
+
return this._mutationThread.push([...arguments]);
|
|
1290
|
+
}
|
|
1291
|
+
this._mutationThreadRunning = true;
|
|
1292
|
+
|
|
1293
|
+
//while ( i < max ) {
|
|
1294
|
+
// atoms[i]
|
|
1295
|
+
// && atoms[i].bagRefs
|
|
1296
|
+
// && atoms[i].bagRefs.length
|
|
1297
|
+
// && atoms[i].bagRefs.each(id => {
|
|
1298
|
+
// allRefs[id] = allRefs[id] || { count: 0 };
|
|
1299
|
+
// allRefs[id].count++;
|
|
1300
|
+
// });
|
|
1301
|
+
// i++;
|
|
1302
|
+
//}
|
|
1303
|
+
|
|
1304
|
+
push.apply(stack, template);
|
|
1305
|
+
|
|
1306
|
+
// if ( !keepRev ) {
|
|
1307
|
+
revId = atomId || shortid.generate();
|
|
1308
|
+
// }
|
|
1309
|
+
|
|
1310
|
+
// !keepRev &&
|
|
1311
|
+
revNum = this._rev;
|
|
1312
|
+
this._rev++;
|
|
1313
|
+
// !keepRev && push.bind(this._revs[this._rev].tpl, template);
|
|
1314
|
+
|
|
1315
|
+
// parse objects...
|
|
1316
|
+
while ( cTplObject = stack.shift() ) {
|
|
1317
|
+
|
|
1318
|
+
// create id & map innertpl ids
|
|
1319
|
+
refId = cTplObject.$_id && this.getRef(cTplObject.$_id, refScope,
|
|
1320
|
+
null,
|
|
1321
|
+
null,
|
|
1322
|
+
// required to work when referencing inner tpl from graph items
|
|
1323
|
+
( id ) => {
|
|
1324
|
+
return (refScope[id] || this.getEtty(id)) && {
|
|
1325
|
+
get: ( key ) => (
|
|
1326
|
+
refScope[id] && refScope[id].hasOwnProperty(key)
|
|
1327
|
+
? refScope[id][key]
|
|
1328
|
+
: this.getEtty(id) && this.getEtty(id).get(key)
|
|
1329
|
+
),
|
|
1330
|
+
}
|
|
1331
|
+
});
|
|
1332
|
+
|
|
1333
|
+
if ( refId && !isString(refId) )
|
|
1334
|
+
refId = refId._id;
|
|
1335
|
+
|
|
1336
|
+
if ( cTplObject.$$_id ) {
|
|
1337
|
+
refId = cTplObject.$$_id;
|
|
1338
|
+
if ( cTplObject.$$_id == cTplObject._id )
|
|
1339
|
+
delete cTplObject._id;
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
if ( !refId && cTplObject._id ) {
|
|
1343
|
+
if ( refs[cTplObject._id] ) {// if there was a previous tpl item with same ref
|
|
1344
|
+
tid = refs[cTplObject._id];
|
|
1345
|
+
}
|
|
1346
|
+
else if ( this._objById[cTplObject._id] ) // keep id if no objects use it
|
|
1347
|
+
tid = refs[cTplObject._id] = refId || shortid.generate();// force it if refid = $(id) is specified
|
|
1348
|
+
else tid = refs[cTplObject._id] = cTplObject._id;
|
|
1349
|
+
}
|
|
1350
|
+
else if ( refId && cTplObject._id ) {
|
|
1351
|
+
tid = refs[cTplObject._id] = refId;
|
|
1352
|
+
aliases.push(cTplObject._id);
|
|
1353
|
+
refScope[cTplObject._id] = refId;
|
|
1354
|
+
}
|
|
1355
|
+
else
|
|
1356
|
+
tid = refId || shortid.generate(); // if the node inherit some other node keep his id
|
|
1357
|
+
|
|
1358
|
+
// if ( keepRev && cTplObject._rev) {// update max rev
|
|
1359
|
+
// this._rev = Math.max(this._rev, cTplObject._rev);
|
|
1360
|
+
// }
|
|
1361
|
+
|
|
1362
|
+
if ( isString(refScope[tid]) ) {
|
|
1363
|
+
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
// now create a pushable object
|
|
1367
|
+
cObject = refScope[tid] = refScope[tid] || { _id: tid, _rev: revNum };
|
|
1368
|
+
// if (!refScope[tid])
|
|
1369
|
+
// refScope[tid] = cObject;
|
|
1370
|
+
|
|
1371
|
+
|
|
1372
|
+
Object.keys(cTplObject).forEach(
|
|
1373
|
+
function ( c ) {
|
|
1374
|
+
if ( c[0] == '$' ) {// auto ref/mount
|
|
1375
|
+
if ( /\$(_incoming|_outgoing|\$?_id)/.test(c) ) return;
|
|
1376
|
+
if ( c === "$$_refMap" ) {
|
|
1377
|
+
|
|
1378
|
+
if ( me.cfg.isMaster ) {// only the master know the real ids
|
|
1379
|
+
pendingRefMaps.push([cTplObject, cTplObject[c], cObject]);
|
|
1380
|
+
}
|
|
1381
|
+
else {
|
|
1382
|
+
// clients keep the map
|
|
1383
|
+
// cObject._refMap = cTplObject._refMap;
|
|
1384
|
+
}
|
|
1385
|
+
return;
|
|
1386
|
+
}
|
|
1387
|
+
var key = c.substr(1);
|
|
1388
|
+
|
|
1389
|
+
if ( c[1] == '$' && isString(cTplObject[c]) ) {// bagRef
|
|
1390
|
+
let bagMngr = cTplObject[c] && me._isBagRefs(cTplObject[c]);
|
|
1391
|
+
if ( bagMngr ) {// if this is an out ref
|
|
1392
|
+
bagRefs[cTplObject[c]] = bagRefs[cTplObject[c]] || { count: 0 };
|
|
1393
|
+
bagRefs[cTplObject[c]].count++;
|
|
1394
|
+
cObject[c.substr(2)] = cTplObject[c];
|
|
1395
|
+
|
|
1396
|
+
// keep ref keys to track them when importing paths
|
|
1397
|
+
cObject.__bagRefKeys = cObject.__bagRefKeys || [];
|
|
1398
|
+
|
|
1399
|
+
!cObject.__bagRefKeys.includes(c.substr(2))
|
|
1400
|
+
&& cObject.__bagRefKeys.push(c.substr(2))
|
|
1401
|
+
}
|
|
1402
|
+
else // if its a string that's an internal ref
|
|
1403
|
+
debug.error("no manager for ref %s", c, cTplObject[c]);
|
|
1404
|
+
}
|
|
1405
|
+
else if ( isString(cTplObject[c]) ) {
|
|
1406
|
+
pendingERefs.push([key, cTplObject[c], cObject]);
|
|
1407
|
+
}
|
|
1408
|
+
else if ( isObject(cTplObject[c]) ) {
|
|
1409
|
+
// a nested object value = a child object to create (Node/Segment/doc);
|
|
1410
|
+
// $_id derives its id from a ref, $$_id forces a literal id
|
|
1411
|
+
if ( cTplObject[c].$_id ) {
|
|
1412
|
+
cTplObject[c]._id = me.getRef(cTplObject[c].$_id, refScope);//@note : cant ref innertpl
|
|
1413
|
+
}
|
|
1414
|
+
if ( cTplObject[c].$$_id ) {
|
|
1415
|
+
cTplObject[c]._id = cTplObject[c].$$_id;//@note : cant ref innertpl
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
cTplObject[c]._id = cTplObject[c]._id || shortid.generate();
|
|
1419
|
+
|
|
1420
|
+
pendingRefs.push([key, cTplObject[c]._id, cObject]);
|
|
1421
|
+
stack.push(cTplObject[c]);
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
}
|
|
1425
|
+
else {// simple copy
|
|
1426
|
+
if ( /(_incoming|_outgoing|\$?_id)/.test(c) ) return;
|
|
1427
|
+
if ( cTplObject[c] !== undefined )
|
|
1428
|
+
cObject[c] = cTplObject[c];
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
);
|
|
1432
|
+
|
|
1433
|
+
// push incomings...
|
|
1434
|
+
cTplObject._incoming
|
|
1435
|
+
&& stack.push.apply(stack, cTplObject._incoming);
|
|
1436
|
+
cTplObject._outgoing
|
|
1437
|
+
&& stack.push.apply(stack, cTplObject._outgoing);
|
|
1438
|
+
}
|
|
1439
|
+
this._taskFlow.wait();
|
|
1440
|
+
this._preloadBagRefs(
|
|
1441
|
+
bagRefs,
|
|
1442
|
+
() => {
|
|
1443
|
+
|
|
1444
|
+
pendingRefs.map(function ( ref ) {
|
|
1445
|
+
ref[2][ref[0]] = ref[1];// apply inner references
|
|
1446
|
+
});
|
|
1447
|
+
pendingERefs.map(( ref ) => {
|
|
1448
|
+
if ( refs[ref[1]] ) {
|
|
1449
|
+
// local alias
|
|
1450
|
+
ref[2][ref[0]] = refs[ref[1]];
|
|
1451
|
+
}
|
|
1452
|
+
else {
|
|
1453
|
+
// if ( /_currentTask/.test(ref[1]) )
|
|
1454
|
+
ref[2][ref[0]] = me.getRef(
|
|
1455
|
+
ref[1],
|
|
1456
|
+
refScope,
|
|
1457
|
+
null,
|
|
1458
|
+
null,
|
|
1459
|
+
// required to work when referencing inner tpl from graph items
|
|
1460
|
+
( id ) => {
|
|
1461
|
+
return (refScope[id] || this.getEtty(id)) && {
|
|
1462
|
+
get: ( key ) => (
|
|
1463
|
+
refScope[id] && refScope[id].hasOwnProperty(key) ? refScope[id][key]
|
|
1464
|
+
: this.getEtty(id) && this.getEtty(id).get(key)
|
|
1465
|
+
),
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
)
|
|
1469
|
+
;// apply outer references
|
|
1470
|
+
}
|
|
1471
|
+
});
|
|
1472
|
+
// build ref map...
|
|
1473
|
+
if ( pendingRefMaps.length ) {
|
|
1474
|
+
// Object.keys(refScope).forEach(
|
|
1475
|
+
// function ( id ) {
|
|
1476
|
+
//
|
|
1477
|
+
// if ( aliases.includes(id) ) {
|
|
1478
|
+
// refMap[id] = refScope[id];
|
|
1479
|
+
// return null;
|
|
1480
|
+
// }
|
|
1481
|
+
// refMap[id] = refScope[id]._id;
|
|
1482
|
+
// });
|
|
1483
|
+
|
|
1484
|
+
pendingRefMaps.map(( ref ) => {
|
|
1485
|
+
if ( isString(ref[1]) ) {
|
|
1486
|
+
debug.error(me._objById[ref[2]._id] && me._objById[ref[2]._id]._etty._._refMap);
|
|
1487
|
+
}
|
|
1488
|
+
delete ref[0].$$_refMap;
|
|
1489
|
+
ref[2]._refMap = ref[0]._refMap =
|
|
1490
|
+
isString(ref[1]) ? {
|
|
1491
|
+
...(me._objById[ref[2]._id] && me._objById[ref[2]._id]._etty._._refMap || {}),
|
|
1492
|
+
[ref[1]]: { ...refs }
|
|
1493
|
+
}
|
|
1494
|
+
: isObject(ref[1]) ? { ...ref[1], ...refs }
|
|
1495
|
+
: { ...refs } // reset with/false
|
|
1496
|
+
});
|
|
1497
|
+
}
|
|
1498
|
+
// we still need to instantiate them..
|
|
1499
|
+
Object.keys(refScope).forEach(
|
|
1500
|
+
function ( id ) {// 1st pass : the nodes
|
|
1501
|
+
|
|
1502
|
+
if ( aliases.includes(id) ) {
|
|
1503
|
+
// refMap[id] = refScope[id];
|
|
1504
|
+
return null;
|
|
1505
|
+
}
|
|
1506
|
+
// refMap[id] = refScope[id]._id;
|
|
1507
|
+
|
|
1508
|
+
|
|
1509
|
+
if ( me._objById[id] ) {// if this is an existing node
|
|
1510
|
+
if ( me._objById[id]._etty._.Node ) {
|
|
1511
|
+
|
|
1512
|
+
// do merge with existing
|
|
1513
|
+
me._objById[id]._etty.update(refScope[id], me);
|
|
1514
|
+
|
|
1515
|
+
me.toggleGraphObjectState(id, "unstable");
|
|
1516
|
+
return me._objById[id];
|
|
1517
|
+
}
|
|
1518
|
+
else pendingObjects.push(id);
|
|
1519
|
+
}
|
|
1520
|
+
else {
|
|
1521
|
+
if ( refScope[id].Node ) {
|
|
1522
|
+
refScope[id]._origin = refScope[id]._origin || baseOrigin || targetId;
|
|
1523
|
+
me._objById[id] =
|
|
1524
|
+
refScope[id].Node && new Node(refScope[id], me);
|
|
1525
|
+
|
|
1526
|
+
|
|
1527
|
+
me._nodes.push(me._objById[id]);
|
|
1528
|
+
me._unstable.push(me._objById[id]);
|
|
1529
|
+
return me._objById[id];
|
|
1530
|
+
}
|
|
1531
|
+
else pendingObjects.push(id);
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
);
|
|
1535
|
+
pendingObjects.map(
|
|
1536
|
+
function ( id ) {// 2nd pass : the segments (they will be auto linked to the nodes..)
|
|
1537
|
+
if ( refScope[id].targetNode == "initialTarget" )
|
|
1538
|
+
revTpl.push(refScope[id]);
|
|
1539
|
+
if ( me._objById[id] ) {// the segment/doc exist
|
|
1540
|
+
// do merge with existing
|
|
1541
|
+
me._objById[id]._etty.update(refScope[id], me);
|
|
1542
|
+
|
|
1543
|
+
me.toggleGraphObjectState(id, "unstable");
|
|
1544
|
+
return me._objById[id];
|
|
1545
|
+
}
|
|
1546
|
+
else {
|
|
1547
|
+
if ( refScope[id].Segment ) {// create the segment
|
|
1548
|
+
|
|
1549
|
+
refScope[id]._origin = refScope[id]._origin || baseOrigin || targetId;
|
|
1550
|
+
me._objById[id] = new Segment(refScope[id], me);
|
|
1551
|
+
|
|
1552
|
+
me._segments.push(me._objById[id]);
|
|
1553
|
+
me._unstable.push(me._objById[id]);
|
|
1554
|
+
// me.toggleGraphObjectState(id, "unstable");
|
|
1555
|
+
me._objById[id]._etty.updateApplicableConcepts(me);
|
|
1556
|
+
return me._objById[id];
|
|
1557
|
+
}
|
|
1558
|
+
else {// records/docs
|
|
1559
|
+
refScope[id]._origin = refScope[id]._origin || baseOrigin || targetId;
|
|
1560
|
+
me._objById[id] = { _etty: new Entity(refScope[id], me) };
|
|
1561
|
+
|
|
1562
|
+
me._segments.push(me._objById[id]);
|
|
1563
|
+
me._unstable.push(me._objById[id]);
|
|
1564
|
+
return me._objById[id];
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
);
|
|
1569
|
+
delete refScope._parent;
|
|
1570
|
+
|
|
1571
|
+
|
|
1572
|
+
this._revs[revNum] = {
|
|
1573
|
+
id : revId,
|
|
1574
|
+
parent: targetId,
|
|
1575
|
+
bagRefs,
|
|
1576
|
+
tpl : Object.keys(refScope).map(
|
|
1577
|
+
id => {
|
|
1578
|
+
if ( aliases.includes(id) ) {
|
|
1579
|
+
refMap[id] = refScope[id];
|
|
1580
|
+
return null;
|
|
1581
|
+
}
|
|
1582
|
+
refMap[id] = refScope[id]._id;
|
|
1583
|
+
let item = { ...refScope[id] };
|
|
1584
|
+
|
|
1585
|
+
item.$$_id = item._id;
|
|
1586
|
+
delete item._id;
|
|
1587
|
+
return item;
|
|
1588
|
+
}
|
|
1589
|
+
).filter(i => !!i)
|
|
1590
|
+
};
|
|
1591
|
+
this._on.mutation
|
|
1592
|
+
&& this._on.mutation.map(( cb ) => cb(me));
|
|
1593
|
+
this.cfg.onMutationApplied
|
|
1594
|
+
&& this.cfg.onMutationApplied(this);
|
|
1595
|
+
// concept-apply trace: this mutation was produced by a concept apply
|
|
1596
|
+
// (applyCtx threaded from Concept.applyTo). Host/sync mutations have no
|
|
1597
|
+
// applyCtx -> no record. prompt/reply are merged from traceProvider.
|
|
1598
|
+
if ( applyCtx && (this.cfg.onConceptApply || (this._on && this._on.conceptApply)) ) {
|
|
1599
|
+
var _rec = {
|
|
1600
|
+
rev : revNum,
|
|
1601
|
+
conceptId : applyCtx.conceptId,
|
|
1602
|
+
conceptName: applyCtx.conceptName,
|
|
1603
|
+
targetId : applyCtx.targetId,
|
|
1604
|
+
applyId : applyCtx.applyId,// joins this trace record to the apply's logs (graph.logger.tail({applyId}))
|
|
1605
|
+
kind : applyCtx.kind,
|
|
1606
|
+
patch : this._revs[revNum] && this._revs[revNum].tpl,
|
|
1607
|
+
bagRefs : this._revs[revNum] && this._revs[revNum].bagRefs,
|
|
1608
|
+
ms : applyCtx.ms,
|
|
1609
|
+
why : applyCtx.why
|
|
1610
|
+
};
|
|
1611
|
+
var _k = applyCtx.conceptId + '/' + applyCtx.targetId;
|
|
1612
|
+
if ( this._traceByApply && this._traceByApply[_k] ) {
|
|
1613
|
+
_rec.prompt = this._traceByApply[_k].prompt;
|
|
1614
|
+
_rec.reply = this._traceByApply[_k].reply;
|
|
1615
|
+
delete this._traceByApply[_k];
|
|
1616
|
+
}
|
|
1617
|
+
this.cfg.onConceptApply && this.cfg.onConceptApply(_rec);
|
|
1618
|
+
this._on.conceptApply && this._on.conceptApply.slice(0).map(( fn ) => fn(me, _rec));
|
|
1619
|
+
}
|
|
1620
|
+
this._mutationThreadRunning = false;
|
|
1621
|
+
this._taskFlow.release();
|
|
1622
|
+
cb && cb(refScope)
|
|
1623
|
+
if ( this._mutationThread.length ) {
|
|
1624
|
+
this.pushMutation(...this._mutationThread.shift())
|
|
1625
|
+
}
|
|
1626
|
+
this.stabilize();
|
|
1627
|
+
|
|
1628
|
+
}
|
|
1629
|
+
);
|
|
1630
|
+
|
|
1631
|
+
return refScope;
|
|
1632
|
+
},
|
|
1633
|
+
|
|
1634
|
+
// -------------------------------------------------------------------------- control
|
|
1635
|
+
|
|
1636
|
+
/**
|
|
1637
|
+
* Launch a stabilisation on all unstable objects
|
|
1638
|
+
* then call cb
|
|
1639
|
+
* @param cb
|
|
1640
|
+
*/
|
|
1641
|
+
stabilize: function ( cb ) {
|
|
1642
|
+
var me = this;
|
|
1643
|
+
cb && this.on("stabilize", function stabilize() {
|
|
1644
|
+
me.un("stabilize", stabilize);
|
|
1645
|
+
cb(arguments);
|
|
1646
|
+
});
|
|
1647
|
+
if ( !this._taskFlow.running ) {
|
|
1648
|
+
this._taskFlow.run();
|
|
1649
|
+
}
|
|
1650
|
+
this._running = true;
|
|
1651
|
+
|
|
1652
|
+
},
|
|
1653
|
+
/**
|
|
1654
|
+
* Call the sync method passed in the cfg (should send last atoms to the server/client)
|
|
1655
|
+
* @param _cb
|
|
1656
|
+
*/
|
|
1657
|
+
sync: function ( _cb ) {
|
|
1658
|
+
var debug = this._log;
|
|
1659
|
+
var me = this,
|
|
1660
|
+
token = this.cfg.doSync
|
|
1661
|
+
&& this.cfg.doSync(this, _cb);
|
|
1662
|
+
debug.log('RT Push request:', token);
|
|
1663
|
+
|
|
1664
|
+
if ( _cb && token ) {
|
|
1665
|
+
this._syncTokens[token] = _cb;
|
|
1666
|
+
}
|
|
1667
|
+
else _cb && _cb();
|
|
1668
|
+
},
|
|
1669
|
+
/**
|
|
1670
|
+
* mk all object unstable
|
|
1671
|
+
*/
|
|
1672
|
+
destabilizeThemAll: function () {
|
|
1673
|
+
Object.keys(this._objById).map(( k ) => this.toggleGraphObjectState(k, 'unstable'), this);
|
|
1674
|
+
},
|
|
1675
|
+
/**
|
|
1676
|
+
* Change some object State (dirty way..)
|
|
1677
|
+
* @param id
|
|
1678
|
+
* @param state
|
|
1679
|
+
* @returns {boolean}
|
|
1680
|
+
*/
|
|
1681
|
+
toggleGraphObjectState: function ( id, state ) {
|
|
1682
|
+
var i,
|
|
1683
|
+
out1, out2, in1,
|
|
1684
|
+
obj = this._objById[id];
|
|
1685
|
+
|
|
1686
|
+
if ( state == "stable" )
|
|
1687
|
+
out1 = this._pending,
|
|
1688
|
+
out2 = this._unstable,
|
|
1689
|
+
in1 = this._stable;
|
|
1690
|
+
else if ( state == "pending" )
|
|
1691
|
+
out1 = this._stable,
|
|
1692
|
+
out2 = this._unstable,
|
|
1693
|
+
in1 = this._pending;
|
|
1694
|
+
else if ( state == "unstable" )
|
|
1695
|
+
out1 = this._stable,
|
|
1696
|
+
out2 = this._pending,
|
|
1697
|
+
in1 = this._unstable;
|
|
1698
|
+
else
|
|
1699
|
+
return false;
|
|
1700
|
+
|
|
1701
|
+
if ( (i = out1.indexOf(obj)) != -1 )
|
|
1702
|
+
out1.splice(i, 1);
|
|
1703
|
+
else if ( (i = out2.indexOf(obj)) != -1 )
|
|
1704
|
+
out2.splice(i, 1);
|
|
1705
|
+
else
|
|
1706
|
+
return false;
|
|
1707
|
+
|
|
1708
|
+
in1.push(obj);
|
|
1709
|
+
return true;
|
|
1710
|
+
},
|
|
1711
|
+
|
|
1712
|
+
// -------------------------------------------------------------------------- accessors
|
|
1713
|
+
|
|
1714
|
+
/**
|
|
1715
|
+
* get a resultpath (paths from getPaths) and return an PathMap object
|
|
1716
|
+
* @param id
|
|
1717
|
+
* @returns {*|null}
|
|
1718
|
+
*/
|
|
1719
|
+
getOpenPathOf : function ( id ) {
|
|
1720
|
+
return this._objById[id]
|
|
1721
|
+
&& this._objById[id]._etty._.OpenPaths
|
|
1722
|
+
&& new PathMap(this._objById[id]._etty._.OpenPaths, this._objById[id]._etty);
|
|
1723
|
+
},
|
|
1724
|
+
removeObj : function ( id, justClean ) {
|
|
1725
|
+
var obj = this._objById[id], i;
|
|
1726
|
+
i = this._pending.indexOf(obj);
|
|
1727
|
+
(i != -1) && this._pending.splice(i, 1);
|
|
1728
|
+
i = this._unstable.indexOf(obj);
|
|
1729
|
+
(i != -1) && this._unstable.splice(i, 1);
|
|
1730
|
+
i = this._stable.indexOf(obj);
|
|
1731
|
+
(i != -1) && this._stable.splice(i, 1);
|
|
1732
|
+
delete this._objById[id];
|
|
1733
|
+
!justClean && obj._etty.destroy(true);
|
|
1734
|
+
},
|
|
1735
|
+
getConcept : function ( id ) {
|
|
1736
|
+
return this._conceptLib[id];
|
|
1737
|
+
},
|
|
1738
|
+
getExtOpenConcepts: function ( id ) {
|
|
1739
|
+
if ( this._objById[id] ) {
|
|
1740
|
+
this._objById[id]._etty.updateApplicableConcepts();// update in case of ..
|
|
1741
|
+
return this._objById[id]._etty._extOpenConcepts;
|
|
1742
|
+
}
|
|
1743
|
+
return [];
|
|
1744
|
+
},
|
|
1745
|
+
|
|
1746
|
+
// --- kept for reference (R&D): travel-domain path-merge / TimePeriod rollup ---
|
|
1747
|
+
// Application-specific (Stay/Travel/childPaths/UserRecord); not part of the V1 core
|
|
1748
|
+
// stabilization path. Retained as a worked example of collapsing a discovered path
|
|
1749
|
+
// (Graph.getPaths) back into the graph as a single segment with child paths.
|
|
1750
|
+
pushPath: function ( path, edgeId, name, cb ) {
|
|
1751
|
+
var debug = this._log;
|
|
1752
|
+
var
|
|
1753
|
+
me = this,
|
|
1754
|
+
scope = this.getEtty(edgeId),
|
|
1755
|
+
// cmaps = path._pmap.maps,
|
|
1756
|
+
getAllPropsInPath = PathMap.prototype.getAllPropsInPath,
|
|
1757
|
+
tm = getAllPropsInPath(path, "TimePeriod"),
|
|
1758
|
+
originId = getAllPropsInPath(path, ["_id", "isTravelStart"])[0],
|
|
1759
|
+
travelEnds = getAllPropsInPath(path, ["_id", "isTravelEnd"]),
|
|
1760
|
+
targetId = travelEnds[travelEnds.length - 1],
|
|
1761
|
+
pathId = shortid.generate(),
|
|
1762
|
+
tpl = path.tpl.map(( obj ) => ({ ...obj, pathId })),
|
|
1763
|
+
rpath,
|
|
1764
|
+
originSrc = scope.getRef('originNode'),
|
|
1765
|
+
originTarget = scope.getRef('targetNode'),
|
|
1766
|
+
tId = tpl[1]._id,
|
|
1767
|
+
tId2 = tpl[tpl.length - 2]._id;//shortid.generate();
|
|
1768
|
+
|
|
1769
|
+
// rm origin target & origin
|
|
1770
|
+
// tpl.shift();
|
|
1771
|
+
tpl.shift();
|
|
1772
|
+
tpl.pop();
|
|
1773
|
+
// tpl.pop();
|
|
1774
|
+
// tpl = tpl
|
|
1775
|
+
tpl[0].originNode = scope._.originNode;
|
|
1776
|
+
tpl[tpl.length - 1].targetNode = scope._.targetNode;
|
|
1777
|
+
//this.pushMutationFromPath(tpl, path.descr, edgeId);
|
|
1778
|
+
rpath = [
|
|
1779
|
+
...path.relatedTpl.map(
|
|
1780
|
+
( m ) => {
|
|
1781
|
+
return { ...m, $_id: '$' + m._id };// use the existing one if exist
|
|
1782
|
+
}
|
|
1783
|
+
),
|
|
1784
|
+
...this.getMutationFromPath(tpl, path.descr, edgeId),
|
|
1785
|
+
{
|
|
1786
|
+
$_id : '$' + edgeId,
|
|
1787
|
+
OpenPaths: false,
|
|
1788
|
+
// Stay : null,
|
|
1789
|
+
// Travel : null,
|
|
1790
|
+
PathIgnore: !scope._.KeepInPath,// <- /!\ this will hide the segment in the debug graph and navline
|
|
1791
|
+
// paths
|
|
1792
|
+
// targetNode : originTarget,
|
|
1793
|
+
// originNode : null,
|
|
1794
|
+
childPaths: {
|
|
1795
|
+
...(scope._.childPaths || {}),
|
|
1796
|
+
[name]: pathId
|
|
1797
|
+
},
|
|
1798
|
+
TimePeriod: null
|
|
1799
|
+
},
|
|
1800
|
+
{
|
|
1801
|
+
$_id : "$UserRecord",
|
|
1802
|
+
loadingSteps: false,
|
|
1803
|
+
// cFocusedEdge : tId2,
|
|
1804
|
+
staysCount: me.selectMapsId(["Stay"], ["VendorStep"]).length
|
|
1805
|
+
}
|
|
1806
|
+
];
|
|
1807
|
+
// if (
|
|
1808
|
+
// scope._.targetNode == "target" ||
|
|
1809
|
+
// (scope.getRef("targetNode:Theoric") && edgeId !== "_root" && !scope.getRef("originNode:Theoric") )
|
|
1810
|
+
// ) {// root must move the initial seg
|
|
1811
|
+
debug.verbose("pushPath rpath", rpath)
|
|
1812
|
+
|
|
1813
|
+
this.pushMutation(
|
|
1814
|
+
rpath,
|
|
1815
|
+
edgeId
|
|
1816
|
+
);
|
|
1817
|
+
// --- kept for reference (R&D): the alternate root-segment-moving branch ---
|
|
1818
|
+
// } else
|
|
1819
|
+
// this.pushMutation(
|
|
1820
|
+
// [
|
|
1821
|
+
//
|
|
1822
|
+
// // {
|
|
1823
|
+
// // $_id : '$' + tId,
|
|
1824
|
+
// // _origin : '_root',
|
|
1825
|
+
// // },
|
|
1826
|
+
// // {
|
|
1827
|
+
// // $_id : '$' + tId2,
|
|
1828
|
+
// // "Undefined" : true,
|
|
1829
|
+
// // _origin : '_root',
|
|
1830
|
+
// // // fxdhfgdgfhdgdg: targetId._id
|
|
1831
|
+
// // },
|
|
1832
|
+
// {
|
|
1833
|
+
// $_id : '$' + edgeId,
|
|
1834
|
+
// OpenPaths : false,
|
|
1835
|
+
// // Stay : null,
|
|
1836
|
+
// // Travel : true,
|
|
1837
|
+
// // targetNode : targetId._id,
|
|
1838
|
+
// Undefined : false,
|
|
1839
|
+
// childPaths : {
|
|
1840
|
+
// ...(scope._.childPaths || {}),
|
|
1841
|
+
// [name || pathId] : pathId
|
|
1842
|
+
// },
|
|
1843
|
+
// // TimePeriod : null
|
|
1844
|
+
// },
|
|
1845
|
+
// {
|
|
1846
|
+
// $_id : "$UserRecord",
|
|
1847
|
+
// loadingSteps : false,
|
|
1848
|
+
// cFocusedEdge : tId2,
|
|
1849
|
+
// staysCount : me.selectMapsId(["Stay"], ["VendorStep"]).length
|
|
1850
|
+
// }
|
|
1851
|
+
// ],
|
|
1852
|
+
// edgeId
|
|
1853
|
+
// );
|
|
1854
|
+
this.stabilize(() => {
|
|
1855
|
+
let newPath = this.getChildPath(edgeId),
|
|
1856
|
+
nextTheo = newPath.reduce(( r, item ) => (item._etty._.Undefined && item._etty._._id || r), edgeId);
|
|
1857
|
+
|
|
1858
|
+
|
|
1859
|
+
cb && cb(nextTheo, newPath);
|
|
1860
|
+
});
|
|
1861
|
+
},
|
|
1862
|
+
/**
|
|
1863
|
+
*
|
|
1864
|
+
* @param origin
|
|
1865
|
+
*/
|
|
1866
|
+
getChildMatching: function ( edgeId, query ) {
|
|
1867
|
+
let newPath = this.getChildPath(edgeId),
|
|
1868
|
+
fn = compileScopeQuery(query, false),
|
|
1869
|
+
nextTheo = newPath.filter(( item ) => fn(item._etty));
|
|
1870
|
+
|
|
1871
|
+
return nextTheo
|
|
1872
|
+
},
|
|
1873
|
+
/**
|
|
1874
|
+
*
|
|
1875
|
+
* @param origin
|
|
1876
|
+
*/
|
|
1877
|
+
isTheoricChildOf: function ( cId, pId ) {
|
|
1878
|
+
let child = this.getEtty(cId),
|
|
1879
|
+
current = child;
|
|
1880
|
+
|
|
1881
|
+
while ( current ) {
|
|
1882
|
+
if ( current._._id == pId )
|
|
1883
|
+
return true;
|
|
1884
|
+
|
|
1885
|
+
current = this.getEtty(current._._origin);
|
|
1886
|
+
}
|
|
1887
|
+
return false;
|
|
1888
|
+
},
|
|
1889
|
+
/**
|
|
1890
|
+
*
|
|
1891
|
+
* @param origin
|
|
1892
|
+
*/
|
|
1893
|
+
getChildPath: function ( origin, forceNoTheoric, idOnly ) {
|
|
1894
|
+
var debug = this._log;
|
|
1895
|
+
// we want paths
|
|
1896
|
+
var map = this._objById,
|
|
1897
|
+
including = isArray(forceNoTheoric) && forceNoTheoric.length ? forceNoTheoric : false,
|
|
1898
|
+
edge = map[origin || "_root"],
|
|
1899
|
+
from = origin && edge._etty.getRef('originNode') || "start",
|
|
1900
|
+
to = origin && edge._etty.getRef('targetNode') || "target",
|
|
1901
|
+
cnode = from, i, path = [from], found, sid, nid, subPath, cEdge;
|
|
1902
|
+
// forceNoTheoric = including;
|
|
1903
|
+
|
|
1904
|
+
// origin = origin || "_root";
|
|
1905
|
+
do {
|
|
1906
|
+
found = false;
|
|
1907
|
+
for ( i = 0; i < map[cnode]._outgoing.length; i++ ) {
|
|
1908
|
+
sid = map[cnode]._outgoing[i];
|
|
1909
|
+
nid = map[sid]._etty._.targetNode;
|
|
1910
|
+
cEdge = map[sid]._etty._;
|
|
1911
|
+
|
|
1912
|
+
if ( sid == origin && map[cnode]._outgoing.length > 1 ) continue;
|
|
1913
|
+
|
|
1914
|
+
if ( (!cEdge.PathIgnore || (cEdge.PathIgnore && cEdge.KeepInPath)) && cEdge._origin == origin ) {
|
|
1915
|
+
if ( path.indexOf(nid) != -1 ) debug.error("This graph have loops", path, nid);
|
|
1916
|
+
|
|
1917
|
+
if ( cEdge.Theoric && forceNoTheoric && (!including || including.includes(cEdge._id)) ) {// so get the complete child path
|
|
1918
|
+
subPath = this.getChildPath(sid, true, true);
|
|
1919
|
+
if ( subPath.length ) {
|
|
1920
|
+
subPath.shift();
|
|
1921
|
+
path.push(...subPath);
|
|
1922
|
+
}
|
|
1923
|
+
else
|
|
1924
|
+
path.push(sid, nid);
|
|
1925
|
+
}
|
|
1926
|
+
else
|
|
1927
|
+
path.push(sid, nid);
|
|
1928
|
+
|
|
1929
|
+
cnode = nid;
|
|
1930
|
+
found = true;
|
|
1931
|
+
break;
|
|
1932
|
+
}
|
|
1933
|
+
else {
|
|
1934
|
+
continue;
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1937
|
+
if ( cnode === to )
|
|
1938
|
+
break;
|
|
1939
|
+
}
|
|
1940
|
+
|
|
1941
|
+
if ( !found ) {// take first
|
|
1942
|
+
sid = map[cnode]._outgoing[0];
|
|
1943
|
+
|
|
1944
|
+
if ( sid == origin ) sid = map[cnode]._outgoing[1];
|
|
1945
|
+
|
|
1946
|
+
if ( !sid ) {
|
|
1947
|
+
return [];
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1950
|
+
path.push(sid, map[sid]._etty._.targetNode);
|
|
1951
|
+
cnode = map[sid]._etty._.targetNode;
|
|
1952
|
+
found = true;
|
|
1953
|
+
}
|
|
1954
|
+
|
|
1955
|
+
if ( cnode === to )
|
|
1956
|
+
break;
|
|
1957
|
+
} while ( 1 );
|
|
1958
|
+
// if ( origin == "_root" )
|
|
1959
|
+
|
|
1960
|
+
|
|
1961
|
+
return idOnly && path || path.map(( id ) => map[id]);
|
|
1962
|
+
},
|
|
1963
|
+
|
|
1964
|
+
/**
|
|
1965
|
+
* Get a all paths between fromId&toId & return them in json
|
|
1966
|
+
* @param fromId
|
|
1967
|
+
* @param toId
|
|
1968
|
+
* @param ignoreMissing
|
|
1969
|
+
* @returns {{maps: {}, paths: Array}}
|
|
1970
|
+
*/
|
|
1971
|
+
getPaths: function ( fromId, toId, skip, opts ) {
|
|
1972
|
+
var debug = this._log;
|
|
1973
|
+
var map = this._objById,
|
|
1974
|
+
cmaps = {},
|
|
1975
|
+
start = this._objById[fromId],
|
|
1976
|
+
end = this._objById[toId],
|
|
1977
|
+
paths = [],
|
|
1978
|
+
stack = [],
|
|
1979
|
+
skipping = skip || [],
|
|
1980
|
+
// ZOOM over the abstraction stack (§9.3-§9.4): by default getPaths prefers the
|
|
1981
|
+
// concrete realization of a theoric (haveNoTheoric) so it returns the fully
|
|
1982
|
+
// EXPANDED path. `collapse` returns the abstract method-level path instead
|
|
1983
|
+
// (theoric hops kept, their realized bodies hidden = "carry the contract, not the
|
|
1984
|
+
// body"); `zoom` is the set of theoric ids to descend INTO even while collapsing
|
|
1985
|
+
// (open one box). Default (no opts) is unchanged & backward compatible.
|
|
1986
|
+
collapse = !!(opts && opts.collapse),
|
|
1987
|
+
zoom = (opts && opts.zoom) || [],
|
|
1988
|
+
related,
|
|
1989
|
+
haveNoTheoric,
|
|
1990
|
+
cpath = [fromId],
|
|
1991
|
+
cnode, i, newPath, sid, nid, cEdge;
|
|
1992
|
+
|
|
1993
|
+
if ( !map[fromId] )
|
|
1994
|
+
debug.error(this._id, "GetPath from node can't be found in the graph", fromId);
|
|
1995
|
+
if ( !map[toId] )
|
|
1996
|
+
debug.error(this._id, "GetPath from node can't be found in the graph", toId);
|
|
1997
|
+
if ( !map[toId] || !map[fromId] )
|
|
1998
|
+
return {
|
|
1999
|
+
maps : cmaps,
|
|
2000
|
+
paths: paths
|
|
2001
|
+
};
|
|
2002
|
+
cmaps[fromId] = { ...map[fromId]._etty._ };
|
|
2003
|
+
|
|
2004
|
+
do {
|
|
2005
|
+
cnode = cpath[cpath.length - 1];//last is node
|
|
2006
|
+
haveNoTheoric = map[cnode]._outgoing.reduce(
|
|
2007
|
+
( p, c ) => {
|
|
2008
|
+
return p || !map[c]._etty._.Theoric
|
|
2009
|
+
}, false);// knowing if there only theoric ways
|
|
2010
|
+
|
|
2011
|
+
for ( i = 0; i < map[cnode]._outgoing.length; i++ ) {
|
|
2012
|
+
newPath = cpath.slice();
|
|
2013
|
+
sid = map[cnode]._outgoing[i];
|
|
2014
|
+
cEdge = map[sid]._etty._;
|
|
2015
|
+
nid = cEdge.targetNode;// node
|
|
2016
|
+
|
|
2017
|
+
if ( collapse ) {
|
|
2018
|
+
// abstraction-stack traversal: keep theoric METHOD hops, hide their
|
|
2019
|
+
// realized bodies — unless a method is explicitly being ZOOMED into.
|
|
2020
|
+
if ( cEdge.Theoric ) {
|
|
2021
|
+
if ( zoom.includes(sid) ) continue;// zoom in: skip the hop, take the body
|
|
2022
|
+
}
|
|
2023
|
+
else if ( cEdge._origin && map[cEdge._origin]
|
|
2024
|
+
&& map[cEdge._origin]._etty._.Theoric
|
|
2025
|
+
&& !zoom.includes(cEdge._origin) )
|
|
2026
|
+
continue;// a body step of a collapsed method -> hidden
|
|
2027
|
+
}
|
|
2028
|
+
// default: ignore theoric if a concrete realization is available
|
|
2029
|
+
else if ( haveNoTheoric && cEdge.Theoric )
|
|
2030
|
+
continue;
|
|
2031
|
+
|
|
2032
|
+
// ignore theoric if possible
|
|
2033
|
+
if ( skipping.includes(sid) )
|
|
2034
|
+
continue;
|
|
2035
|
+
|
|
2036
|
+
if ( (related = map[sid]._etty._.pathDescriptor) && map[related] ) {
|
|
2037
|
+
do {
|
|
2038
|
+
cmaps[related] = { ...map[related]._etty._ };
|
|
2039
|
+
} while ( related = map[related] && map[related]._etty._.parentPathDescriptor );// assume broken
|
|
2040
|
+
// refs are good in
|
|
2041
|
+
// other graphs
|
|
2042
|
+
}
|
|
2043
|
+
|
|
2044
|
+
if ( (related = map[sid]._etty._._origin) && map[related] ) {// add _origin ( the theoric which has generated this segment )
|
|
2045
|
+
|
|
2046
|
+
do {
|
|
2047
|
+
// if (map[related] && map[related]._etty._.thisOne)
|
|
2048
|
+
|
|
2049
|
+
if ( skipping.includes(related) )
|
|
2050
|
+
break;
|
|
2051
|
+
|
|
2052
|
+
cmaps[related] = { ...map[related]._etty._ };
|
|
2053
|
+
related = map[related] && map[related]._etty._._origin
|
|
2054
|
+
|
|
2055
|
+
} while ( related && map[related] && !cmaps[related] );
|
|
2056
|
+
}
|
|
2057
|
+
|
|
2058
|
+
newPath.push(sid, nid);
|
|
2059
|
+
|
|
2060
|
+
if ( !cmaps[sid] )
|
|
2061
|
+
cmaps[sid] = { ...map[sid]._etty._ };
|
|
2062
|
+
// if ( !cmaps[nid] && !map[nid]){
|
|
2063
|
+
// nid,
|
|
2064
|
+
// map[sid]._etty._
|
|
2065
|
+
// )
|
|
2066
|
+
// }
|
|
2067
|
+
if ( !cmaps[nid] )
|
|
2068
|
+
cmaps[nid] = { ...map[nid]._etty._ };
|
|
2069
|
+
|
|
2070
|
+
if ( nid === toId )
|
|
2071
|
+
paths.push(newPath);
|
|
2072
|
+
else
|
|
2073
|
+
stack.push(newPath);
|
|
2074
|
+
}
|
|
2075
|
+
|
|
2076
|
+
if ( !stack.length ) break;
|
|
2077
|
+
cpath = stack.shift();
|
|
2078
|
+
} while ( 1 );
|
|
2079
|
+
|
|
2080
|
+
return {
|
|
2081
|
+
maps : cmaps,
|
|
2082
|
+
paths: paths
|
|
2083
|
+
};
|
|
2084
|
+
},
|
|
2085
|
+
|
|
2086
|
+
/**
|
|
2087
|
+
* We need to know if a vendor record is mounted from some point,
|
|
2088
|
+
* or we'll not have any method to know if a vendorRecord is still available
|
|
2089
|
+
*
|
|
2090
|
+
* @param to
|
|
2091
|
+
* @param manager // should contain getFormByRecordId, checkRecordValidityById, ...
|
|
2092
|
+
*/
|
|
2093
|
+
registerVendorRecordByKey: function ( to, manager ) {
|
|
2094
|
+
},
|
|
2095
|
+
/**
|
|
2096
|
+
* Get an object by his id
|
|
2097
|
+
* @param id
|
|
2098
|
+
* @returns {*}
|
|
2099
|
+
*/
|
|
2100
|
+
getObjById: function ( id ) {
|
|
2101
|
+
return this._objById[id];
|
|
2102
|
+
},
|
|
2103
|
+
|
|
2104
|
+
/**
|
|
2105
|
+
* Get the concept map by his id
|
|
2106
|
+
* @param id
|
|
2107
|
+
* @returns {*}
|
|
2108
|
+
*/
|
|
2109
|
+
getEtty: function ( id ) {
|
|
2110
|
+
return this._objById[id] && this._objById[id]._etty;
|
|
2111
|
+
},
|
|
2112
|
+
/**
|
|
2113
|
+
* Select all node that match the query
|
|
2114
|
+
* @param _with Array
|
|
2115
|
+
* @param _without Array
|
|
2116
|
+
* @returns {array|*|Array}
|
|
2117
|
+
*/
|
|
2118
|
+
queryMaps: function ( query ) {
|
|
2119
|
+
|
|
2120
|
+
var me = this,
|
|
2121
|
+
maps = this._objById,
|
|
2122
|
+
fn = compileScopeQuery(query, true)
|
|
2123
|
+
|
|
2124
|
+
|
|
2125
|
+
;
|
|
2126
|
+
return Object.keys(maps).map(( k ) => maps[k]._etty).filter(fn);
|
|
2127
|
+
|
|
2128
|
+
},
|
|
2129
|
+
/**
|
|
2130
|
+
* Select all node with _with prop but whithout _whithout props
|
|
2131
|
+
* @param _with Array
|
|
2132
|
+
* @param _without Array
|
|
2133
|
+
* @returns {array|*|Array}
|
|
2134
|
+
*/
|
|
2135
|
+
selectMaps: function ( _with, _without ) {
|
|
2136
|
+
var me = this;
|
|
2137
|
+
return this.selectMapsId(_with, _without).map(( v ) => me._objById[v] && me._objById[v]._etty);
|
|
2138
|
+
|
|
2139
|
+
},
|
|
2140
|
+
|
|
2141
|
+
/**
|
|
2142
|
+
* Select all node id with _with prop but whithout _whithout props
|
|
2143
|
+
* @param _with Array
|
|
2144
|
+
* @param _without Array
|
|
2145
|
+
* @returns {array|*|Array}
|
|
2146
|
+
*/
|
|
2147
|
+
selectMapsId: function ( _with, _without ) {
|
|
2148
|
+
var i = 0, me = this, maps = this._mapsByConcept,
|
|
2149
|
+
_with = isArray(_with) ? _with : [_with],
|
|
2150
|
+
_have,
|
|
2151
|
+
_without = isArray(_without) ? _without : [_without];
|
|
2152
|
+
|
|
2153
|
+
|
|
2154
|
+
_with.map(( v ) => maps[v] && (_have = intersect(_have || maps[v], maps[v])));
|
|
2155
|
+
_have && _without.map(( v ) => maps[v] && (_have = arrayDiffer(_have, maps[v])));
|
|
2156
|
+
|
|
2157
|
+
|
|
2158
|
+
return _have || [];
|
|
2159
|
+
|
|
2160
|
+
},
|
|
2161
|
+
|
|
2162
|
+
// -------------------------------------------------------------------------- events
|
|
2163
|
+
|
|
2164
|
+
on: function ( evt, cb ) {
|
|
2165
|
+
if ( !isFunction(cb) ) throw 'wtf';
|
|
2166
|
+
|
|
2167
|
+
this._on[evt] = this._on[evt] || [];
|
|
2168
|
+
this._on[evt].push(cb);
|
|
2169
|
+
|
|
2170
|
+
},
|
|
2171
|
+
un: function ( evt, cb ) {
|
|
2172
|
+
//this._on[evt] = this._on[evt]||[];
|
|
2173
|
+
if ( !this._on[evt] ) return;
|
|
2174
|
+
var i = this._on[evt].indexOf(cb);
|
|
2175
|
+
this._on[evt].splice(i, 1);
|
|
2176
|
+
},
|
|
2177
|
+
/**
|
|
2178
|
+
* Provider trace hook: lets a provider report the prompt/reply (or any extra
|
|
2179
|
+
* payload) it produced for the current apply, WITHOUT coupling the engine to
|
|
2180
|
+
* providers. No-op unless a concept-apply trace sink is configured. The payload
|
|
2181
|
+
* is merged into the trace record emitted when the provider's mutation lands.
|
|
2182
|
+
* Keyed by concept+target so concurrent async providers don't collide.
|
|
2183
|
+
* @param concept the concept (provider's 2nd arg)
|
|
2184
|
+
* @param scope the scope Entity (provider's 3rd arg)
|
|
2185
|
+
* @param payload e.g. { prompt, reply }
|
|
2186
|
+
*/
|
|
2187
|
+
traceProvider: function ( concept, scope, payload ) {
|
|
2188
|
+
if ( !this.cfg.onConceptApply && !(this._on && this._on.conceptApply) ) return;
|
|
2189
|
+
(this._traceByApply = this._traceByApply || {})[concept._id + '/' + scope._._id] = payload;
|
|
2190
|
+
},
|
|
2191
|
+
/**
|
|
2192
|
+
* Called once stabilized
|
|
2193
|
+
* @private
|
|
2194
|
+
*/
|
|
2195
|
+
_applyStabilized: function () {
|
|
2196
|
+
this._log.info('graph settled (fixpoint) at rev %s', this._rev);
|
|
2197
|
+
var me = this;
|
|
2198
|
+
this._stabilized = true;
|
|
2199
|
+
this._stabilizing = false;// pass complete — host ops (incl. those issued from onStabilize) apply immediately again
|
|
2200
|
+
this._applyCount = {};// healthy settle ends the episode — clear the per-(target/concept) apply tally (#11.c.1)
|
|
2201
|
+
this._lastSettledRev = this.getCurrentRevision();// last clean checkpoint (has a snapshot) for a reactive supervisor's rollback (#11.c.4)
|
|
2202
|
+
me._running = false;
|
|
2203
|
+
// me._rev++;// graph inst revision
|
|
2204
|
+
this._captureSnapshot();// checkpoint this coherent state so rollbackTo() can restore it
|
|
2205
|
+
this._on.stabilize
|
|
2206
|
+
&& this._on.stabilize.slice(0).map(( cb ) => cb(me, me._syncTokensList));
|
|
2207
|
+
this.cfg.onStabilize
|
|
2208
|
+
&& this.cfg.onStabilize(this, me._syncTokensList);
|
|
2209
|
+
me._syncTokensList = [];
|
|
2210
|
+
},
|
|
2211
|
+
history_push : function ( mutation, targetId, isStep ) {
|
|
2212
|
+
},
|
|
2213
|
+
history_goto : function ( to ) {
|
|
2214
|
+
return this.rollbackTo(to);
|
|
2215
|
+
},
|
|
2216
|
+
// -------------------------------------------------------------------------- rollback
|
|
2217
|
+
/**
|
|
2218
|
+
* Checkpoint the current stabilized (coherent) state so rollbackTo() can
|
|
2219
|
+
* restore it later. Keyed by the current revision; no-op if already captured.
|
|
2220
|
+
* Snapshots are full serialized states (delta replay is left for later).
|
|
2221
|
+
* @private
|
|
2222
|
+
*/
|
|
2223
|
+
_captureSnapshot: function () {
|
|
2224
|
+
this._snapshots = this._snapshots || {};
|
|
2225
|
+
var rev = this.getCurrentRevision();
|
|
2226
|
+
if ( !this._snapshots[rev] ) {
|
|
2227
|
+
var snap = this.serialize();// { lastRev, graph } — the FACT state
|
|
2228
|
+
// N6 (#11.c.2): also snapshot the FULL live concept schema tree, so rollbackTo()
|
|
2229
|
+
// restores the rules (runtime add/patchConcept), not just the facts. Cached and
|
|
2230
|
+
// invalidated on each concept-lib edit, so unchanged libs aren't re-serialized.
|
|
2231
|
+
snap.concepts = this._conceptSnapshot || (this._conceptSnapshot = this._serializeConceptTree());
|
|
2232
|
+
this._snapshots[rev] = snap;
|
|
2233
|
+
|
|
2234
|
+
// Bounded revision log (opt-in, R11.6): in a long-running/live regime _snapshots +
|
|
2235
|
+
// _revs grow unbounded (each settle/mutation appends). `cfg.maxRevisions` keeps a
|
|
2236
|
+
// retained window [rev-cap, rev]; a rollbackTo() BEYOND the window hits the existing
|
|
2237
|
+
// "no snapshot for revision" throw, so the only effect is a bounded history (the
|
|
2238
|
+
// retained snapshots still restore exactly). Default unset = unbounded (no change).
|
|
2239
|
+
var cap = this.cfg && this.cfg.maxRevisions;
|
|
2240
|
+
if ( cap > 0 && rev - cap > 0 ) {
|
|
2241
|
+
var floor = rev - cap;
|
|
2242
|
+
for ( var k in this._snapshots ) if ( Number(k) < floor ) delete this._snapshots[k];
|
|
2243
|
+
for ( var r = 0; r < floor; r++ ) if ( this._revs[r] != null ) delete this._revs[r];
|
|
2244
|
+
}
|
|
2245
|
+
}
|
|
2246
|
+
},
|
|
2247
|
+
/**
|
|
2248
|
+
* Serialize the LIVE concept tree (reflecting runtime add/patchConcept) into a nested
|
|
2249
|
+
* record `new Concept(...)` can rebuild — walking `_openConcepts` (so adds are caught)
|
|
2250
|
+
* and reading each concept's current `_schema` (so patches are caught; the parent's
|
|
2251
|
+
* `_schema.childConcepts` is NOT authoritative after a patch). JSON-cloned (schemas are
|
|
2252
|
+
* JSON-safe: providers are string refs, no functions). (#11.c.2 / N6)
|
|
2253
|
+
*/
|
|
2254
|
+
_serializeConceptTree: function () {
|
|
2255
|
+
function ser( c ) {
|
|
2256
|
+
var out = {}, s = c._schema || {};
|
|
2257
|
+
Object.keys(s).forEach(function ( k ) { if ( k !== 'childConcepts' ) out[k] = s[k]; });
|
|
2258
|
+
if ( c._openConcepts ) {
|
|
2259
|
+
var ids = Object.keys(c._openConcepts);
|
|
2260
|
+
if ( ids.length ) {
|
|
2261
|
+
out.childConcepts = {};
|
|
2262
|
+
ids.forEach(function ( id ) { out.childConcepts[id] = ser(c._openConcepts[id]); });
|
|
2263
|
+
}
|
|
2264
|
+
}
|
|
2265
|
+
return out;
|
|
2266
|
+
}
|
|
2267
|
+
return JSON.parse(JSON.stringify(ser(this._rootConcept)));
|
|
2268
|
+
},
|
|
2269
|
+
/**
|
|
2270
|
+
* Public: the LIVE concept tree as a serializable record (reflecting runtime add/patchConcept).
|
|
2271
|
+
* The inverse of the `conceptMap` you boot with — feed it to corpus-pack#packCorpus for a
|
|
2272
|
+
* portable `.sgc` bundle, or to load.js#exportConceptsToDir to write the on-disk JSONC layout.
|
|
2273
|
+
* @returns {{childConcepts:object}} a tree `new Graph(..., { <set>: tree })` can reload.
|
|
2274
|
+
*/
|
|
2275
|
+
exportConcepts: function () {
|
|
2276
|
+
return this._serializeConceptTree();
|
|
2277
|
+
},
|
|
2278
|
+
/**
|
|
2279
|
+
* Rebuild the concept library from a snapshotted schema tree (deep-cloned so later
|
|
2280
|
+
* edits don't mutate the stored snapshot). Used by rollbackTo. (#11.c.2 / N6)
|
|
2281
|
+
*/
|
|
2282
|
+
_restoreConceptTree: function ( tree ) {
|
|
2283
|
+
this._conceptLib = {};
|
|
2284
|
+
this._mapsByConcept = this._mapsByConcept || {};
|
|
2285
|
+
this._rootConcept = new Concept(JSON.parse(JSON.stringify(tree)), this);
|
|
2286
|
+
this._conceptSnapshot = null;// the live lib changed — force a fresh capture next settle
|
|
2287
|
+
},
|
|
2288
|
+
/**
|
|
2289
|
+
* @returns {number[]} revisions available to rollbackTo(), ascending
|
|
2290
|
+
*/
|
|
2291
|
+
getRevisions: function () {
|
|
2292
|
+
return Object.keys(this._snapshots || {}).map(Number).sort(( a, b ) => a - b);
|
|
2293
|
+
},
|
|
2294
|
+
/**
|
|
2295
|
+
* The serialized snapshot captured at `revisionNumber` (as serialize() yields:
|
|
2296
|
+
* `{ lastRev, graph: "<json>" }`), or null if no snapshot was captured for it.
|
|
2297
|
+
* @param revisionNumber
|
|
2298
|
+
*/
|
|
2299
|
+
getSnapshot: function ( revisionNumber ) {
|
|
2300
|
+
return (this._snapshots && this._snapshots[revisionNumber]) || null;
|
|
2301
|
+
},
|
|
2302
|
+
/**
|
|
2303
|
+
* Parse a snapshot into a { objectId -> facts } map (the serialized
|
|
2304
|
+
* conceptMaps, keyed by _id). `_rev` is dropped (volatile per-object marker).
|
|
2305
|
+
* @private
|
|
2306
|
+
*/
|
|
2307
|
+
_snapshotFacts: function ( snap ) {
|
|
2308
|
+
var maps = (JSON.parse(snap.graph).conceptMaps) || [], by = {};
|
|
2309
|
+
maps.forEach(function ( m ) {
|
|
2310
|
+
var f = {};
|
|
2311
|
+
Object.keys(m).forEach(function ( k ) { if ( k !== '_rev' ) f[k] = m[k]; });
|
|
2312
|
+
by[m._id] = f;
|
|
2313
|
+
});
|
|
2314
|
+
return by;
|
|
2315
|
+
},
|
|
2316
|
+
/**
|
|
2317
|
+
* Diff two captured revisions: what revision `b` added / removed / changed
|
|
2318
|
+
* versus revision `a`. The inspection layer of "Git for reasoning".
|
|
2319
|
+
*
|
|
2320
|
+
* @param a revision (from getRevisions())
|
|
2321
|
+
* @param b revision (from getRevisions())
|
|
2322
|
+
* @returns {{added:Object, removed:Object, changed:Object}}
|
|
2323
|
+
* added/removed: { id -> facts }; changed: { id -> { key -> [beforeVal, afterVal] } }
|
|
2324
|
+
* @throws if either revision has no snapshot
|
|
2325
|
+
*/
|
|
2326
|
+
diffRevisions: function ( a, b ) {
|
|
2327
|
+
var snapA = this.getSnapshot(a), snapB = this.getSnapshot(b);
|
|
2328
|
+
if ( !snapA ) throw new Error("diffRevisions: no snapshot for revision " + a);
|
|
2329
|
+
if ( !snapB ) throw new Error("diffRevisions: no snapshot for revision " + b);
|
|
2330
|
+
|
|
2331
|
+
var fa = this._snapshotFacts(snapA), fb = this._snapshotFacts(snapB),
|
|
2332
|
+
added = {}, removed = {}, changed = {};
|
|
2333
|
+
|
|
2334
|
+
Object.keys(fb).forEach(function ( id ) { if ( !fa[id] ) added[id] = fb[id]; });
|
|
2335
|
+
Object.keys(fa).forEach(function ( id ) { if ( !fb[id] ) removed[id] = fa[id]; });
|
|
2336
|
+
Object.keys(fa).forEach(function ( id ) {
|
|
2337
|
+
if ( !fb[id] ) return;
|
|
2338
|
+
var keys = {}, d = {};
|
|
2339
|
+
Object.keys(fa[id]).forEach(function ( k ) { keys[k] = 1; });
|
|
2340
|
+
Object.keys(fb[id]).forEach(function ( k ) { keys[k] = 1; });
|
|
2341
|
+
Object.keys(keys).forEach(function ( k ) {
|
|
2342
|
+
if ( JSON.stringify(fa[id][k]) !== JSON.stringify(fb[id][k]) )
|
|
2343
|
+
d[k] = [fa[id][k], fb[id][k]];
|
|
2344
|
+
});
|
|
2345
|
+
if ( Object.keys(d).length ) changed[id] = d;
|
|
2346
|
+
});
|
|
2347
|
+
return { added: added, removed: removed, changed: changed };
|
|
2348
|
+
},
|
|
2349
|
+
/**
|
|
2350
|
+
* Roll the whole graph back to a previously stabilized revision: re-mount that
|
|
2351
|
+
* snapshot and re-stabilize (re-fires onStabilize). Snapshots strictly after
|
|
2352
|
+
* `revisionNumber` are discarded — this is a linear undo, the restored timeline
|
|
2353
|
+
* replaces the abandoned one.
|
|
2354
|
+
*
|
|
2355
|
+
* @param revisionNumber a revision from getRevisions() / getCurrentRevision()
|
|
2356
|
+
* @returns {number} the restored revision
|
|
2357
|
+
* @throws {Error} if no snapshot exists for that revision
|
|
2358
|
+
*/
|
|
2359
|
+
rollbackTo: function ( revisionNumber ) {
|
|
2360
|
+
var snap = this._snapshots && this._snapshots[revisionNumber];
|
|
2361
|
+
if ( !snap )
|
|
2362
|
+
throw new Error("rollbackTo: no snapshot for revision " + revisionNumber +
|
|
2363
|
+
" (available: " + this.getRevisions().join(', ') + ")");
|
|
2364
|
+
|
|
2365
|
+
// N6 (#11.c.2): restore the concept LIBRARY first (before mount re-evaluates objects
|
|
2366
|
+
// against it), so a runtime add/patchConcept made after this rev is undone too — else
|
|
2367
|
+
// a surviving concept re-casts and the rolled-back edit "resurrects".
|
|
2368
|
+
if ( snap.concepts ) this._restoreConceptTree(snap.concepts);
|
|
2369
|
+
|
|
2370
|
+
// Issued mid-stabilize (e.g. from a supervisor concept's provider)? Defer to the
|
|
2371
|
+
// quiescent _loopTF boundary — rollbackTo re-mounts, which must not happen mid-pass
|
|
2372
|
+
// (#11.c.4). Multiple requests collapse to the EARLIEST rev (roll back furthest).
|
|
2373
|
+
if ( this._stabilizing ) {
|
|
2374
|
+
this._pendingRollback = (this._pendingRollback == null)
|
|
2375
|
+
? revisionNumber : Math.min(this._pendingRollback, revisionNumber);
|
|
2376
|
+
return revisionNumber;
|
|
2377
|
+
}
|
|
2378
|
+
|
|
2379
|
+
this._doRollback(revisionNumber);
|
|
2380
|
+
this._taskFlow.run();// re-stabilize -> re-fires _applyStabilized / onStabilize
|
|
2381
|
+
this._running = true;
|
|
2382
|
+
return revisionNumber;
|
|
2383
|
+
},
|
|
2384
|
+
/**
|
|
2385
|
+
* The re-mount body of rollbackTo WITHOUT the stabilize kick — restore the concept
|
|
2386
|
+
* lib (N6) then the facts, and drop the abandoned future. Used by the public method
|
|
2387
|
+
* (host path, then kicks) and by the mid-stabilize drain (`_loopTF`, no kick — the
|
|
2388
|
+
* re-arm re-stabilizes since mount leaves objects unstable). (#11.c.4)
|
|
2389
|
+
*/
|
|
2390
|
+
_doRollback: function ( revisionNumber ) {
|
|
2391
|
+
var me = this,
|
|
2392
|
+
snap = this._snapshots && this._snapshots[revisionNumber];
|
|
2393
|
+
if ( !snap ) return;
|
|
2394
|
+
if ( snap.concepts ) this._restoreConceptTree(snap.concepts);// N6: rules first
|
|
2395
|
+
this.mount(JSON.parse(snap.graph));// rebuild _objById at that rev, mark unstable, set _rev
|
|
2396
|
+
Object.keys(this._snapshots).forEach(function ( r ) {
|
|
2397
|
+
if ( Number(r) > revisionNumber ) delete me._snapshots[r];// drop the abandoned future
|
|
2398
|
+
});
|
|
2399
|
+
this._stabilized = false;
|
|
2400
|
+
},
|
|
2401
|
+
// -------------------------------------------------------------------------- fork / merge
|
|
2402
|
+
/**
|
|
2403
|
+
* Fork an independent child Graph (a sub-agent sandbox) to develop a path /
|
|
2404
|
+
* sub-problem on its own, optionally with a different concept set (= different
|
|
2405
|
+
* capabilities). Reuses this graph's concept library unless `conf.conceptMap`
|
|
2406
|
+
* overrides it. Thin wrapper over `new Graph(...)` — no new core machinery.
|
|
2407
|
+
*
|
|
2408
|
+
* @param seed serialized graph / {conceptMaps|nodes|segments} seeding the child;
|
|
2409
|
+
* omitted -> forks this graph's current snapshot (serialize()).
|
|
2410
|
+
* @param conf cfg overrides merged onto this graph's cfg. Extras:
|
|
2411
|
+
* `conceptMap` (override library); `reintegrateInto` (targetId) +
|
|
2412
|
+
* `project` -> auto-merge the child's result back here on its stabilize.
|
|
2413
|
+
* @returns {Graph} the child graph
|
|
2414
|
+
*/
|
|
2415
|
+
fork: function ( seed, conf ) {
|
|
2416
|
+
var me = this;
|
|
2417
|
+
conf = conf || {};
|
|
2418
|
+
var reintegrateInto = conf.reintegrateInto,
|
|
2419
|
+
project = conf.project,
|
|
2420
|
+
userOnStabilize = conf.onStabilize,
|
|
2421
|
+
conceptMap = conf.conceptMap || this._conceptMap;
|
|
2422
|
+
|
|
2423
|
+
var childConf = { ...this.cfg, isMaster: true, autoMount: true, ...conf };
|
|
2424
|
+
delete childConf.reintegrateInto;
|
|
2425
|
+
delete childConf.project;
|
|
2426
|
+
delete childConf.conceptMap;
|
|
2427
|
+
|
|
2428
|
+
if ( reintegrateInto != null ) {
|
|
2429
|
+
childConf.onStabilize = function ( child, tokens ) {
|
|
2430
|
+
userOnStabilize && userOnStabilize(child, tokens);
|
|
2431
|
+
if ( !child._merged ) {
|
|
2432
|
+
child._merged = true;
|
|
2433
|
+
me.merge(child, reintegrateInto, project);
|
|
2434
|
+
}
|
|
2435
|
+
};
|
|
2436
|
+
}
|
|
2437
|
+
|
|
2438
|
+
var record = seed || JSON.parse(this.serialize().graph);
|
|
2439
|
+
var child = new this.static(record, childConf, conceptMap);
|
|
2440
|
+
(this._forks = this._forks || []).push(child);
|
|
2441
|
+
return child;
|
|
2442
|
+
},
|
|
2443
|
+
/**
|
|
2444
|
+
* Reintegrate a forked child's result into this graph: apply project(child) (a
|
|
2445
|
+
* mutation template) onto `targetId`, then destroy the child.
|
|
2446
|
+
*
|
|
2447
|
+
* @param child a Graph returned by fork()
|
|
2448
|
+
* @param targetId the object in THIS graph to merge the result onto
|
|
2449
|
+
* @param project (child) -> mutation template; default attaches child.serialize()
|
|
2450
|
+
* @returns {Graph} this
|
|
2451
|
+
*/
|
|
2452
|
+
merge: function ( child, targetId, project ) {
|
|
2453
|
+
var tpl = project
|
|
2454
|
+
? project(child)
|
|
2455
|
+
: { $$_id: targetId, forkResult: JSON.parse(child.serialize().graph) };
|
|
2456
|
+
if ( tpl ) this.pushMutation(tpl, targetId, true);
|
|
2457
|
+
|
|
2458
|
+
var i = this._forks ? this._forks.indexOf(child) : -1;
|
|
2459
|
+
if ( i !== -1 ) this._forks.splice(i, 1);
|
|
2460
|
+
child && !child._dead && child.destroy && child.destroy();
|
|
2461
|
+
return this;
|
|
2462
|
+
},
|
|
2463
|
+
/**
|
|
2464
|
+
* clean & unref
|
|
2465
|
+
*/
|
|
2466
|
+
destroy: function () {
|
|
2467
|
+
this._taskFlow.kill();
|
|
2468
|
+
var me = this;
|
|
2469
|
+
this._on.destroy
|
|
2470
|
+
&& this._on.destroy.slice(0).map(( cb ) => cb(me));
|
|
2471
|
+
Object.keys(this._objById).map(
|
|
2472
|
+
( k ) => me._objById[k].destroy ? me._objById[k].destroy() : me._objById[k]._etty.destroy());
|
|
2473
|
+
this._freeNodes = this._nodes = this._objById = me._conceptLib = me._syncTokens =
|
|
2474
|
+
me._syncTokensList = this._segments =
|
|
2475
|
+
this._rootConcept = this._mapsByConcept = this._on =
|
|
2476
|
+
this._history = this._unstable = this._pending = this._stable = this._objById = this._pendingMutationsById = null;
|
|
2477
|
+
this._dead = true;
|
|
2478
|
+
}
|
|
2479
|
+
};
|
|
2480
|
+
|
|
2481
|
+
// The debugger/host logging interface: graph.logger.addSink(fn) / removeSink /
|
|
2482
|
+
// tail(n, {concept|target|applyId|level}) / records / setLevel(name). Logs live
|
|
2483
|
+
// here (bounded ring buffer + sinks), never as facts on the graph objects.
|
|
2484
|
+
Object.defineProperty(Graph.prototype, 'logger', { get: function () { return this._log; } });
|
|
2485
|
+
|
|
2486
|
+
module.exports = Graph;
|