@continuum-dev/session 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +306 -0
- package/index.d.ts +3 -0
- package/index.d.ts.map +1 -0
- package/index.js +2 -0
- package/lib/session/action-manager.d.ts +7 -0
- package/lib/session/action-manager.d.ts.map +1 -0
- package/lib/session/checkpoint-manager.d.ts +39 -0
- package/lib/session/checkpoint-manager.d.ts.map +1 -0
- package/lib/session/checkpoint-manager.js +106 -0
- package/lib/session/destroyer.d.ts +12 -0
- package/lib/session/destroyer.d.ts.map +1 -0
- package/lib/session/destroyer.js +22 -0
- package/lib/session/event-log.d.ts +13 -0
- package/lib/session/event-log.d.ts.map +1 -0
- package/lib/session/event-log.js +126 -0
- package/lib/session/intent-manager.d.ts +34 -0
- package/lib/session/intent-manager.d.ts.map +1 -0
- package/lib/session/intent-manager.js +67 -0
- package/lib/session/listeners.d.ts +49 -0
- package/lib/session/listeners.d.ts.map +1 -0
- package/lib/session/listeners.js +77 -0
- package/lib/session/persistence.d.ts +14 -0
- package/lib/session/persistence.d.ts.map +1 -0
- package/lib/session/persistence.js +99 -0
- package/lib/session/schema-pusher.d.ts +4 -0
- package/lib/session/schema-pusher.d.ts.map +1 -0
- package/lib/session/serializer.d.ts +25 -0
- package/lib/session/serializer.d.ts.map +1 -0
- package/lib/session/serializer.js +126 -0
- package/lib/session/session-state.d.ts +65 -0
- package/lib/session/session-state.d.ts.map +1 -0
- package/lib/session/session-state.js +79 -0
- package/lib/session/view-pusher.d.ts +13 -0
- package/lib/session/view-pusher.d.ts.map +1 -0
- package/lib/session/view-pusher.js +104 -0
- package/lib/session.d.ts +33 -0
- package/lib/session.d.ts.map +1 -0
- package/lib/session.js +275 -0
- package/lib/types.d.ts +265 -0
- package/lib/types.d.ts.map +1 -0
- package/lib/types.js +1 -0
- package/package.json +46 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ViewDefinition } from '@continuum/contract';
|
|
2
|
+
import type { SessionState } from './session-state.js';
|
|
3
|
+
/**
|
|
4
|
+
* Pushes a new view definition into the session and reconciles existing data.
|
|
5
|
+
*
|
|
6
|
+
* Updates reconciliation artifacts, marks stale pending intents when view version
|
|
7
|
+
* changes, creates an auto checkpoint, runs detached-value GC, and notifies listeners.
|
|
8
|
+
*
|
|
9
|
+
* @param internal Mutable internal session state.
|
|
10
|
+
* @param view Next view definition to apply.
|
|
11
|
+
*/
|
|
12
|
+
export declare function pushView(internal: SessionState, view: ViewDefinition): void;
|
|
13
|
+
//# sourceMappingURL=view-pusher.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"view-pusher.d.ts","sourceRoot":"","sources":["../../../../../packages/session/src/lib/session/view-pusher.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAiB,MAAM,qBAAqB,CAAC;AAEzE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AA+EvD;;;;;;;;GAQG;AACH,wBAAgB,QAAQ,CAAC,QAAQ,EAAE,YAAY,EAAE,IAAI,EAAE,cAAc,GAAG,IAAI,CAiC3E"}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { reconcile } from '@continuum/runtime';
|
|
2
|
+
import { autoCheckpoint } from './checkpoint-manager.js';
|
|
3
|
+
import { markAllPendingIntentsAsStale } from './intent-manager.js';
|
|
4
|
+
import { notifySnapshotAndIssueListeners } from './listeners.js';
|
|
5
|
+
function assertValidView(view) {
|
|
6
|
+
if (typeof view.viewId !== 'string' || view.viewId.length === 0) {
|
|
7
|
+
throw new Error('Invalid view: "viewId" must be a non-empty string');
|
|
8
|
+
}
|
|
9
|
+
if (typeof view.version !== 'string' || view.version.length === 0) {
|
|
10
|
+
throw new Error('Invalid view: "version" must be a non-empty string');
|
|
11
|
+
}
|
|
12
|
+
if (!Array.isArray(view.nodes)) {
|
|
13
|
+
throw new Error('Invalid view: "nodes" must be an array');
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function runDetachedValueGC(internal) {
|
|
17
|
+
const policy = internal.detachedValuePolicy;
|
|
18
|
+
const detached = internal.currentData?.detachedValues;
|
|
19
|
+
if (!policy || !detached || Object.keys(detached).length === 0) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const now = internal.clock();
|
|
23
|
+
const entries = Object.entries(detached);
|
|
24
|
+
const toRemove = new Set();
|
|
25
|
+
// Increment pushesSinceDetach for all entries
|
|
26
|
+
for (const [, value] of entries) {
|
|
27
|
+
value.pushesSinceDetach = (value.pushesSinceDetach ?? 0) + 1;
|
|
28
|
+
}
|
|
29
|
+
// Strategy: maxAge
|
|
30
|
+
if (policy.maxAge !== undefined) {
|
|
31
|
+
for (const [key, value] of entries) {
|
|
32
|
+
if (now - value.detachedAt > policy.maxAge) {
|
|
33
|
+
toRemove.add(key);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
// Strategy: pushCount
|
|
38
|
+
if (policy.pushCount !== undefined) {
|
|
39
|
+
for (const [key, value] of entries) {
|
|
40
|
+
if ((value.pushesSinceDetach ?? 0) >= policy.pushCount) {
|
|
41
|
+
toRemove.add(key);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
// Strategy: maxCount (FIFO — oldest first)
|
|
46
|
+
if (policy.maxCount !== undefined) {
|
|
47
|
+
const remaining = entries
|
|
48
|
+
.filter(([key]) => !toRemove.has(key))
|
|
49
|
+
.sort(([, a], [, b]) => a.detachedAt - b.detachedAt);
|
|
50
|
+
while (remaining.length > policy.maxCount) {
|
|
51
|
+
const oldest = remaining.shift();
|
|
52
|
+
toRemove.add(oldest[0]);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (toRemove.size === 0)
|
|
56
|
+
return;
|
|
57
|
+
const updated = {};
|
|
58
|
+
for (const [key, value] of entries) {
|
|
59
|
+
if (!toRemove.has(key)) {
|
|
60
|
+
updated[key] = value;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (Object.keys(updated).length === 0) {
|
|
64
|
+
const { detachedValues: _, ...rest } = internal.currentData;
|
|
65
|
+
internal.currentData = rest;
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
internal.currentData = { ...internal.currentData, detachedValues: updated };
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Pushes a new view definition into the session and reconciles existing data.
|
|
73
|
+
*
|
|
74
|
+
* Updates reconciliation artifacts, marks stale pending intents when view version
|
|
75
|
+
* changes, creates an auto checkpoint, runs detached-value GC, and notifies listeners.
|
|
76
|
+
*
|
|
77
|
+
* @param internal Mutable internal session state.
|
|
78
|
+
* @param view Next view definition to apply.
|
|
79
|
+
*/
|
|
80
|
+
export function pushView(internal, view) {
|
|
81
|
+
if (internal.destroyed)
|
|
82
|
+
return;
|
|
83
|
+
assertValidView(view);
|
|
84
|
+
const priorVersion = internal.currentView?.version;
|
|
85
|
+
internal.priorView = internal.currentView;
|
|
86
|
+
internal.currentView = view;
|
|
87
|
+
const result = reconcile(view, internal.priorView, internal.currentData, { clock: internal.clock, ...(internal.reconciliationOptions ?? {}) });
|
|
88
|
+
internal.currentData = {
|
|
89
|
+
...result.reconciledState,
|
|
90
|
+
lineage: {
|
|
91
|
+
...result.reconciledState.lineage,
|
|
92
|
+
sessionId: internal.sessionId,
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
internal.issues = result.issues;
|
|
96
|
+
internal.diffs = result.diffs;
|
|
97
|
+
internal.resolutions = result.resolutions;
|
|
98
|
+
if (priorVersion && priorVersion !== view.version) {
|
|
99
|
+
markAllPendingIntentsAsStale(internal);
|
|
100
|
+
}
|
|
101
|
+
autoCheckpoint(internal);
|
|
102
|
+
runDetachedValueGC(internal);
|
|
103
|
+
notifySnapshotAndIssueListeners(internal);
|
|
104
|
+
}
|
package/lib/session.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { Session, SessionOptions, SessionFactory } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Creates a new in-memory session ledger.
|
|
4
|
+
*
|
|
5
|
+
* Initializes event log limits, reconciliation behavior, optional persistence,
|
|
6
|
+
* and optional action handlers.
|
|
7
|
+
*
|
|
8
|
+
* @param options Optional session configuration.
|
|
9
|
+
* @returns A live session instance.
|
|
10
|
+
*/
|
|
11
|
+
export declare function createSession(options?: SessionOptions): Session;
|
|
12
|
+
/**
|
|
13
|
+
* Recreates a session from serialized data produced by `session.serialize()`.
|
|
14
|
+
*
|
|
15
|
+
* @param data Serialized session payload.
|
|
16
|
+
* @param options Optional runtime overrides (clock, limits, reconciliation, persistence).
|
|
17
|
+
* @returns A live session instance restored from the payload.
|
|
18
|
+
*/
|
|
19
|
+
export declare function deserialize(data: unknown, options?: SessionOptions): Session;
|
|
20
|
+
/**
|
|
21
|
+
* Hydrates a session from persistence storage when data exists, otherwise creates a new one.
|
|
22
|
+
*
|
|
23
|
+
* If stored data is invalid, it is removed and a fresh session is created.
|
|
24
|
+
*
|
|
25
|
+
* @param options Session options including required persistence config.
|
|
26
|
+
* @returns A hydrated or newly created session.
|
|
27
|
+
*/
|
|
28
|
+
export declare function hydrateOrCreate(options?: SessionOptions): Session;
|
|
29
|
+
/**
|
|
30
|
+
* Dependency-injection friendly factory for session creation and deserialization.
|
|
31
|
+
*/
|
|
32
|
+
export declare const sessionFactory: SessionFactory;
|
|
33
|
+
//# sourceMappingURL=session.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../../../packages/session/src/lib/session.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AA2M1E;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAAC,OAAO,CAAC,EAAE,cAAc,GAAG,OAAO,CAqB/D;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,cAAc,GAAG,OAAO,CAuB5E;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,OAAO,CAAC,EAAE,cAAc,GAAG,OAAO,CAWjE;AAED;;GAEG;AACH,eAAO,MAAM,cAAc,EAAE,cAA+C,CAAC"}
|
package/lib/session.js
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import { INTERACTION_TYPES } from '@continuum/contract';
|
|
2
|
+
import { createEmptySessionState, generateId, resetSessionState } from './session/session-state.js';
|
|
3
|
+
import { buildSnapshotFromCurrentState, notifySnapshotListeners, subscribeSnapshot, subscribeIssues } from './session/listeners.js';
|
|
4
|
+
import { cloneCheckpointSnapshot, createManualCheckpoint, restoreFromCheckpoint, rewind } from './session/checkpoint-manager.js';
|
|
5
|
+
import { submitIntent, validateIntent, cancelIntent } from './session/intent-manager.js';
|
|
6
|
+
import { recordIntent } from './session/event-log.js';
|
|
7
|
+
import { pushView } from './session/view-pusher.js';
|
|
8
|
+
import { serializeSession, deserializeToState } from './session/serializer.js';
|
|
9
|
+
import { teardownSessionAndClearState } from './session/destroyer.js';
|
|
10
|
+
import { attachPersistence } from './session/persistence.js';
|
|
11
|
+
const DEFAULT_STORAGE_KEY = 'continuum_session';
|
|
12
|
+
const SESSION_DESTROYED_ERROR = 'Session has been destroyed';
|
|
13
|
+
function assertNotDestroyed(internal) {
|
|
14
|
+
if (internal.destroyed) {
|
|
15
|
+
throw new Error(SESSION_DESTROYED_ERROR);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function applyViewportStateUpdate(internal, nodeId, state) {
|
|
19
|
+
if (!internal.currentData) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const now = internal.clock();
|
|
23
|
+
internal.currentData = {
|
|
24
|
+
...internal.currentData,
|
|
25
|
+
viewContext: {
|
|
26
|
+
...(internal.currentData.viewContext ?? {}),
|
|
27
|
+
[nodeId]: state,
|
|
28
|
+
},
|
|
29
|
+
lineage: {
|
|
30
|
+
...internal.currentData.lineage,
|
|
31
|
+
timestamp: now,
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
const lastAutoCheckpoint = [...internal.checkpoints]
|
|
35
|
+
.reverse()
|
|
36
|
+
.find((checkpoint) => checkpoint.trigger === 'auto');
|
|
37
|
+
if (lastAutoCheckpoint) {
|
|
38
|
+
const snapshot = buildSnapshotFromCurrentState(internal);
|
|
39
|
+
if (snapshot) {
|
|
40
|
+
lastAutoCheckpoint.snapshot = cloneCheckpointSnapshot(snapshot);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
notifySnapshotListeners(internal);
|
|
44
|
+
}
|
|
45
|
+
function assembleSessionFromInternalState(internal, cleanupPersistence) {
|
|
46
|
+
const session = {
|
|
47
|
+
get sessionId() { return internal.sessionId; },
|
|
48
|
+
get isDestroyed() { return internal.destroyed; },
|
|
49
|
+
getSnapshot() { assertNotDestroyed(internal); return buildSnapshotFromCurrentState(internal); },
|
|
50
|
+
getIssues() { assertNotDestroyed(internal); return [...internal.issues]; },
|
|
51
|
+
getDiffs() { assertNotDestroyed(internal); return [...internal.diffs]; },
|
|
52
|
+
getResolutions() { assertNotDestroyed(internal); return [...internal.resolutions]; },
|
|
53
|
+
getEventLog() { assertNotDestroyed(internal); return [...internal.eventLog]; },
|
|
54
|
+
getPendingIntents() { assertNotDestroyed(internal); return [...internal.pendingIntents]; },
|
|
55
|
+
getDetachedValues() {
|
|
56
|
+
assertNotDestroyed(internal);
|
|
57
|
+
return { ...(internal.currentData?.detachedValues ?? {}) };
|
|
58
|
+
},
|
|
59
|
+
purgeDetachedValues(filter) {
|
|
60
|
+
assertNotDestroyed(internal);
|
|
61
|
+
if (!internal.currentData?.detachedValues)
|
|
62
|
+
return;
|
|
63
|
+
if (!filter) {
|
|
64
|
+
const { detachedValues: _, ...rest } = internal.currentData;
|
|
65
|
+
internal.currentData = rest;
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
const remaining = {};
|
|
69
|
+
for (const [key, value] of Object.entries(internal.currentData.detachedValues)) {
|
|
70
|
+
if (!filter(key, value)) {
|
|
71
|
+
remaining[key] = value;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (Object.keys(remaining).length === 0) {
|
|
75
|
+
const { detachedValues: _, ...rest } = internal.currentData;
|
|
76
|
+
internal.currentData = rest;
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
internal.currentData = { ...internal.currentData, detachedValues: remaining };
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
notifySnapshotListeners(internal);
|
|
83
|
+
},
|
|
84
|
+
proposeValue(nodeId, value, source) {
|
|
85
|
+
assertNotDestroyed(internal);
|
|
86
|
+
if (!internal.currentData)
|
|
87
|
+
return;
|
|
88
|
+
const existing = internal.currentData.values[nodeId];
|
|
89
|
+
if (existing && existing.isDirty) {
|
|
90
|
+
internal.pendingProposals[nodeId] = {
|
|
91
|
+
nodeId,
|
|
92
|
+
proposedValue: value,
|
|
93
|
+
currentValue: existing,
|
|
94
|
+
proposedAt: internal.clock(),
|
|
95
|
+
source,
|
|
96
|
+
};
|
|
97
|
+
notifySnapshotListeners(internal);
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
recordIntent(internal, { nodeId, type: INTERACTION_TYPES.DATA_UPDATE, payload: value });
|
|
101
|
+
delete internal.pendingProposals[nodeId];
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
acceptProposal(nodeId) {
|
|
105
|
+
assertNotDestroyed(internal);
|
|
106
|
+
const proposal = internal.pendingProposals[nodeId];
|
|
107
|
+
if (!proposal)
|
|
108
|
+
return;
|
|
109
|
+
recordIntent(internal, {
|
|
110
|
+
nodeId,
|
|
111
|
+
type: INTERACTION_TYPES.DATA_UPDATE,
|
|
112
|
+
payload: { ...proposal.proposedValue, isDirty: true }
|
|
113
|
+
});
|
|
114
|
+
delete internal.pendingProposals[nodeId];
|
|
115
|
+
},
|
|
116
|
+
rejectProposal(nodeId) {
|
|
117
|
+
assertNotDestroyed(internal);
|
|
118
|
+
if (!internal.pendingProposals[nodeId]) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
delete internal.pendingProposals[nodeId];
|
|
122
|
+
notifySnapshotListeners(internal);
|
|
123
|
+
},
|
|
124
|
+
getPendingProposals() {
|
|
125
|
+
assertNotDestroyed(internal);
|
|
126
|
+
return { ...internal.pendingProposals };
|
|
127
|
+
},
|
|
128
|
+
getCheckpoints() { assertNotDestroyed(internal); return [...internal.checkpoints]; },
|
|
129
|
+
pushView(view) { assertNotDestroyed(internal); pushView(internal, view); },
|
|
130
|
+
recordIntent(partial) { assertNotDestroyed(internal); recordIntent(internal, partial); },
|
|
131
|
+
updateState(nodeId, payload) {
|
|
132
|
+
assertNotDestroyed(internal);
|
|
133
|
+
recordIntent(internal, { nodeId, type: INTERACTION_TYPES.DATA_UPDATE, payload });
|
|
134
|
+
},
|
|
135
|
+
getViewportState(nodeId) {
|
|
136
|
+
assertNotDestroyed(internal);
|
|
137
|
+
return internal.currentData?.viewContext?.[nodeId];
|
|
138
|
+
},
|
|
139
|
+
updateViewportState(nodeId, state) {
|
|
140
|
+
assertNotDestroyed(internal);
|
|
141
|
+
applyViewportStateUpdate(internal, nodeId, state);
|
|
142
|
+
},
|
|
143
|
+
submitIntent(partial) { assertNotDestroyed(internal); submitIntent(internal, partial); },
|
|
144
|
+
validateIntent(intentId) { assertNotDestroyed(internal); return validateIntent(internal, intentId); },
|
|
145
|
+
cancelIntent(intentId) { assertNotDestroyed(internal); return cancelIntent(internal, intentId); },
|
|
146
|
+
checkpoint() { assertNotDestroyed(internal); return createManualCheckpoint(internal); },
|
|
147
|
+
restoreFromCheckpoint(cp) { assertNotDestroyed(internal); restoreFromCheckpoint(internal, cp); },
|
|
148
|
+
rewind(checkpointId) { assertNotDestroyed(internal); rewind(internal, checkpointId); },
|
|
149
|
+
reset() {
|
|
150
|
+
assertNotDestroyed(internal);
|
|
151
|
+
resetSessionState(internal);
|
|
152
|
+
notifySnapshotListeners(internal);
|
|
153
|
+
},
|
|
154
|
+
onSnapshot(listener) { assertNotDestroyed(internal); return subscribeSnapshot(internal, listener); },
|
|
155
|
+
onIssues(listener) { assertNotDestroyed(internal); return subscribeIssues(internal, listener); },
|
|
156
|
+
serialize() { assertNotDestroyed(internal); return serializeSession(internal); },
|
|
157
|
+
destroy() {
|
|
158
|
+
assertNotDestroyed(internal);
|
|
159
|
+
cleanupPersistence?.();
|
|
160
|
+
return teardownSessionAndClearState(internal);
|
|
161
|
+
},
|
|
162
|
+
registerAction(intentId, registration, handler) {
|
|
163
|
+
assertNotDestroyed(internal);
|
|
164
|
+
internal.actionRegistry.set(intentId, { registration, handler });
|
|
165
|
+
},
|
|
166
|
+
unregisterAction(intentId) {
|
|
167
|
+
assertNotDestroyed(internal);
|
|
168
|
+
internal.actionRegistry.delete(intentId);
|
|
169
|
+
},
|
|
170
|
+
getRegisteredActions() {
|
|
171
|
+
assertNotDestroyed(internal);
|
|
172
|
+
const result = {};
|
|
173
|
+
for (const [id, entry] of internal.actionRegistry) {
|
|
174
|
+
result[id] = entry.registration;
|
|
175
|
+
}
|
|
176
|
+
return result;
|
|
177
|
+
},
|
|
178
|
+
dispatchAction(intentId, nodeId) {
|
|
179
|
+
assertNotDestroyed(internal);
|
|
180
|
+
const entry = internal.actionRegistry.get(intentId);
|
|
181
|
+
if (!entry)
|
|
182
|
+
return;
|
|
183
|
+
const snapshot = buildSnapshotFromCurrentState(internal);
|
|
184
|
+
if (!snapshot)
|
|
185
|
+
return;
|
|
186
|
+
return entry.handler({ intentId, snapshot: snapshot.data, nodeId });
|
|
187
|
+
},
|
|
188
|
+
};
|
|
189
|
+
return session;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Creates a new in-memory session ledger.
|
|
193
|
+
*
|
|
194
|
+
* Initializes event log limits, reconciliation behavior, optional persistence,
|
|
195
|
+
* and optional action handlers.
|
|
196
|
+
*
|
|
197
|
+
* @param options Optional session configuration.
|
|
198
|
+
* @returns A live session instance.
|
|
199
|
+
*/
|
|
200
|
+
export function createSession(options) {
|
|
201
|
+
const clock = options?.clock ?? Date.now;
|
|
202
|
+
const internal = createEmptySessionState(generateId('session', clock), clock);
|
|
203
|
+
internal.maxEventLogSize = options?.maxEventLogSize ?? internal.maxEventLogSize;
|
|
204
|
+
internal.maxPendingIntents = options?.maxPendingIntents ?? internal.maxPendingIntents;
|
|
205
|
+
internal.maxCheckpoints = options?.maxCheckpoints ?? internal.maxCheckpoints;
|
|
206
|
+
internal.reconciliationOptions = options?.reconciliation;
|
|
207
|
+
internal.validateOnUpdate = options?.validateOnUpdate ?? internal.validateOnUpdate;
|
|
208
|
+
internal.detachedValuePolicy = options?.detachedValuePolicy;
|
|
209
|
+
if (options?.actions) {
|
|
210
|
+
for (const [id, entry] of Object.entries(options.actions)) {
|
|
211
|
+
internal.actionRegistry.set(id, entry);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
const cleanupPersistence = options?.persistence
|
|
215
|
+
? attachPersistence(internal, {
|
|
216
|
+
...options.persistence,
|
|
217
|
+
key: options.persistence.key ?? DEFAULT_STORAGE_KEY,
|
|
218
|
+
})
|
|
219
|
+
: undefined;
|
|
220
|
+
return assembleSessionFromInternalState(internal, cleanupPersistence);
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Recreates a session from serialized data produced by `session.serialize()`.
|
|
224
|
+
*
|
|
225
|
+
* @param data Serialized session payload.
|
|
226
|
+
* @param options Optional runtime overrides (clock, limits, reconciliation, persistence).
|
|
227
|
+
* @returns A live session instance restored from the payload.
|
|
228
|
+
*/
|
|
229
|
+
export function deserialize(data, options) {
|
|
230
|
+
const internal = deserializeToState(data, options?.clock ?? Date.now, {
|
|
231
|
+
maxEventLogSize: options?.maxEventLogSize,
|
|
232
|
+
maxPendingIntents: options?.maxPendingIntents,
|
|
233
|
+
maxCheckpoints: options?.maxCheckpoints,
|
|
234
|
+
});
|
|
235
|
+
if (options?.reconciliation) {
|
|
236
|
+
internal.reconciliationOptions = options.reconciliation;
|
|
237
|
+
}
|
|
238
|
+
if (options?.validateOnUpdate !== undefined) {
|
|
239
|
+
internal.validateOnUpdate = options.validateOnUpdate;
|
|
240
|
+
}
|
|
241
|
+
const cleanupPersistence = options?.persistence
|
|
242
|
+
? attachPersistence(internal, {
|
|
243
|
+
...options.persistence,
|
|
244
|
+
key: options.persistence.key ?? DEFAULT_STORAGE_KEY,
|
|
245
|
+
})
|
|
246
|
+
: undefined;
|
|
247
|
+
return assembleSessionFromInternalState(internal, cleanupPersistence);
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Hydrates a session from persistence storage when data exists, otherwise creates a new one.
|
|
251
|
+
*
|
|
252
|
+
* If stored data is invalid, it is removed and a fresh session is created.
|
|
253
|
+
*
|
|
254
|
+
* @param options Session options including required persistence config.
|
|
255
|
+
* @returns A hydrated or newly created session.
|
|
256
|
+
*/
|
|
257
|
+
export function hydrateOrCreate(options) {
|
|
258
|
+
if (!options?.persistence)
|
|
259
|
+
return createSession(options);
|
|
260
|
+
const storageKey = options.persistence.key ?? DEFAULT_STORAGE_KEY;
|
|
261
|
+
const raw = options.persistence.storage.getItem(storageKey);
|
|
262
|
+
if (!raw)
|
|
263
|
+
return createSession(options);
|
|
264
|
+
try {
|
|
265
|
+
return deserialize(JSON.parse(raw), options);
|
|
266
|
+
}
|
|
267
|
+
catch {
|
|
268
|
+
options.persistence.storage.removeItem(storageKey);
|
|
269
|
+
return createSession(options);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Dependency-injection friendly factory for session creation and deserialization.
|
|
274
|
+
*/
|
|
275
|
+
export const sessionFactory = { createSession, deserialize };
|