@continuum-dev/session 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -0
- package/lib/session/checkpoint-manager.d.ts.map +1 -1
- package/lib/session/checkpoint-manager.js +10 -0
- package/lib/session/event-log.d.ts.map +1 -1
- package/lib/session/event-log.js +16 -8
- package/lib/session/listeners.d.ts.map +1 -1
- package/lib/session/listeners.js +7 -1
- package/lib/session/persistence.d.ts.map +1 -1
- package/lib/session/persistence.js +38 -28
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -92,6 +92,7 @@ function hydrateOrCreate(options?: SessionOptions): Session;
|
|
|
92
92
|
Persistence note:
|
|
93
93
|
|
|
94
94
|
- When `options.persistence` is provided, snapshot writes are debounced by 200ms.
|
|
95
|
+
- Pending writes are flushed on `beforeunload` so tab closes do not drop recent updates.
|
|
95
96
|
- `SessionPersistenceOptions.maxBytes` enforces a payload size cap before writes.
|
|
96
97
|
- `SessionPersistenceOptions.onError` receives `size_limit` and `storage_error` events.
|
|
97
98
|
- Browser `storage` events are consumed for cross-tab session synchronization.
|
|
@@ -118,6 +119,8 @@ const stopIssues = session.onIssues((issues) => {
|
|
|
118
119
|
});
|
|
119
120
|
```
|
|
120
121
|
|
|
122
|
+
Snapshot listeners receive immutable top-level copies of `view` and `data`.
|
|
123
|
+
|
|
121
124
|
### 2) View and State Updates
|
|
122
125
|
|
|
123
126
|
#### `session.pushView(view)`
|
|
@@ -148,6 +151,8 @@ session.recordIntent({
|
|
|
148
151
|
});
|
|
149
152
|
```
|
|
150
153
|
|
|
154
|
+
`recordIntent` clones incoming payload objects before storing them and deduplicates issues by `nodeId + code`.
|
|
155
|
+
|
|
151
156
|
#### Viewport APIs
|
|
152
157
|
|
|
153
158
|
```typescript
|
|
@@ -310,6 +315,7 @@ Persistence behavior:
|
|
|
310
315
|
|
|
311
316
|
- `serialize()` returns a JSON-safe payload with `formatVersion: 1`.
|
|
312
317
|
- Automatic persistence writes are debounced (200ms) to reduce storage churn.
|
|
318
|
+
- Pending writes are flushed on `beforeunload` to reduce data loss risk during tab close.
|
|
313
319
|
- If `maxBytes` is exceeded, the write is skipped and `onError` is invoked.
|
|
314
320
|
- Remote storage updates can rehydrate in-memory state for cross-tab continuity.
|
|
315
321
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"checkpoint-manager.d.ts","sourceRoot":"","sources":["../../../../../packages/session/src/lib/session/checkpoint-manager.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AACtD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAOvD;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,GAAG,CAAC,CAEtD;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,YAAY,GAAG,IAAI,CAqB3D;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,YAAY,GAAG,UAAU,
|
|
1
|
+
{"version":3,"file":"checkpoint-manager.d.ts","sourceRoot":"","sources":["../../../../../packages/session/src/lib/session/checkpoint-manager.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AACtD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAOvD;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,GAAG,CAAC,CAEtD;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,YAAY,GAAG,IAAI,CAqB3D;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,YAAY,GAAG,UAAU,CAyBzE;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,YAAY,EAAE,EAAE,EAAE,UAAU,GAAG,IAAI,CAalF;AAED;;;;;GAKG;AACH,wBAAgB,MAAM,CAAC,QAAQ,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,GAAG,IAAI,CAkBzE"}
|
|
@@ -59,6 +59,16 @@ export function createManualCheckpoint(internal) {
|
|
|
59
59
|
trigger: 'manual',
|
|
60
60
|
};
|
|
61
61
|
internal.checkpoints.push(checkpoint);
|
|
62
|
+
if (internal.checkpoints.length > internal.maxCheckpoints) {
|
|
63
|
+
const overflow = internal.checkpoints.length - internal.maxCheckpoints;
|
|
64
|
+
for (let index = 0; index < overflow; index += 1) {
|
|
65
|
+
const removableIndex = internal.checkpoints.findIndex((cp) => cp.trigger === 'manual');
|
|
66
|
+
if (removableIndex === -1) {
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
internal.checkpoints.splice(removableIndex, 1);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
62
72
|
return checkpoint;
|
|
63
73
|
}
|
|
64
74
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"event-log.d.ts","sourceRoot":"","sources":["../../../../../packages/session/src/lib/session/event-log.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAY,WAAW,EAA2B,MAAM,qBAAqB,CAAC;AAE1F,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"event-log.d.ts","sourceRoot":"","sources":["../../../../../packages/session/src/lib/session/event-log.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAY,WAAW,EAA2B,MAAM,qBAAqB,CAAC;AAE1F,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAoEvD;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAC1B,QAAQ,EAAE,YAAY,EACtB,OAAO,EAAE,IAAI,CAAC,WAAW,EAAE,eAAe,GAAG,WAAW,GAAG,WAAW,GAAG,aAAa,CAAC,GACtF,IAAI,CA+EN"}
|
package/lib/session/event-log.js
CHANGED
|
@@ -3,6 +3,16 @@ import { generateId } from './session-state.js';
|
|
|
3
3
|
import { buildSnapshotFromCurrentState, notifySnapshotAndIssueListeners } from './listeners.js';
|
|
4
4
|
import { cloneCheckpointSnapshot } from './checkpoint-manager.js';
|
|
5
5
|
import { validateNodeValue } from '@continuum-dev/runtime';
|
|
6
|
+
function dedupeIssues(existing, incoming) {
|
|
7
|
+
if (incoming.length === 0) {
|
|
8
|
+
return existing;
|
|
9
|
+
}
|
|
10
|
+
const nextKeys = new Set(incoming.map((issue) => `${issue.nodeId ?? ''}:${issue.code}`));
|
|
11
|
+
return [
|
|
12
|
+
...existing.filter((issue) => !nextKeys.has(`${issue.nodeId ?? ''}:${issue.code}`)),
|
|
13
|
+
...incoming,
|
|
14
|
+
];
|
|
15
|
+
}
|
|
6
16
|
function collectNodesByCanonicalId(nodes) {
|
|
7
17
|
const byId = new Map();
|
|
8
18
|
const walk = (items, parentPath) => {
|
|
@@ -75,24 +85,22 @@ export function recordIntent(internal, partial) {
|
|
|
75
85
|
}
|
|
76
86
|
const resolvedEntry = resolveNodeLookupEntry(internal.currentView.nodes, partial.nodeId);
|
|
77
87
|
if (!resolvedEntry) {
|
|
78
|
-
internal.issues = [
|
|
79
|
-
...internal.issues,
|
|
80
|
-
{
|
|
88
|
+
internal.issues = dedupeIssues(internal.issues, [{
|
|
81
89
|
severity: ISSUE_SEVERITY.WARNING,
|
|
82
90
|
nodeId: partial.nodeId,
|
|
83
91
|
message: `Node ${partial.nodeId} not found in current view`,
|
|
84
92
|
code: ISSUE_CODES.UNKNOWN_NODE,
|
|
85
|
-
}
|
|
86
|
-
];
|
|
93
|
+
}]);
|
|
87
94
|
notifySnapshotAndIssueListeners(internal);
|
|
88
95
|
return;
|
|
89
96
|
}
|
|
90
97
|
const { canonicalId, node } = resolvedEntry;
|
|
98
|
+
const payload = { ...partial.payload };
|
|
91
99
|
internal.currentData = {
|
|
92
100
|
...internal.currentData,
|
|
93
101
|
values: {
|
|
94
102
|
...internal.currentData.values,
|
|
95
|
-
[canonicalId]:
|
|
103
|
+
[canonicalId]: payload,
|
|
96
104
|
},
|
|
97
105
|
lineage: {
|
|
98
106
|
...internal.currentData.lineage,
|
|
@@ -108,9 +116,9 @@ export function recordIntent(internal, partial) {
|
|
|
108
116
|
},
|
|
109
117
|
};
|
|
110
118
|
if (internal.validateOnUpdate) {
|
|
111
|
-
const validationIssues = validateNodeValue(node,
|
|
119
|
+
const validationIssues = validateNodeValue(node, payload);
|
|
112
120
|
if (validationIssues.length > 0) {
|
|
113
|
-
internal.issues =
|
|
121
|
+
internal.issues = dedupeIssues(internal.issues, validationIssues);
|
|
114
122
|
}
|
|
115
123
|
}
|
|
116
124
|
const lastAutoCheckpoint = [...internal.checkpoints]
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"listeners.d.ts","sourceRoot":"","sources":["../../../../../packages/session/src/lib/session/listeners.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAC9D,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAC9D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAEvD;;;;;GAKG;AACH,wBAAgB,6BAA6B,CAAC,QAAQ,EAAE,YAAY,GAAG,kBAAkB,GAAG,IAAI,
|
|
1
|
+
{"version":3,"file":"listeners.d.ts","sourceRoot":"","sources":["../../../../../packages/session/src/lib/session/listeners.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAC9D,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAC9D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAEvD;;;;;GAKG;AACH,wBAAgB,6BAA6B,CAAC,QAAQ,EAAE,YAAY,GAAG,kBAAkB,GAAG,IAAI,CAS/F;AAED;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,YAAY,GAAG,IAAI,CASpE;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,YAAY,GAAG,IAAI,CAQjE;AAED;;;;GAIG;AACH,wBAAgB,+BAA+B,CAAC,QAAQ,EAAE,YAAY,GAAG,IAAI,CAG5E;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,YAAY,EACtB,QAAQ,EAAE,CAAC,QAAQ,EAAE,kBAAkB,GAAG,IAAI,KAAK,IAAI,GACtD,MAAM,IAAI,CAGZ;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAC7B,QAAQ,EAAE,YAAY,EACtB,QAAQ,EAAE,CAAC,MAAM,EAAE,mBAAmB,EAAE,KAAK,IAAI,GAChD,MAAM,IAAI,CAGZ"}
|
package/lib/session/listeners.js
CHANGED
|
@@ -7,7 +7,13 @@
|
|
|
7
7
|
export function buildSnapshotFromCurrentState(internal) {
|
|
8
8
|
if (!internal.currentView || !internal.currentData)
|
|
9
9
|
return null;
|
|
10
|
-
|
|
10
|
+
const snapshot = {
|
|
11
|
+
view: { ...internal.currentView },
|
|
12
|
+
data: { ...internal.currentData },
|
|
13
|
+
};
|
|
14
|
+
Object.freeze(snapshot.view);
|
|
15
|
+
Object.freeze(snapshot.data);
|
|
16
|
+
return Object.freeze(snapshot);
|
|
11
17
|
}
|
|
12
18
|
/**
|
|
13
19
|
* Notifies all snapshot listeners with the latest snapshot value.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"persistence.d.ts","sourceRoot":"","sources":["../../../../../packages/session/src/lib/session/persistence.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAIvD,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,aAAa,CAAC;AAkB7D;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,YAAY,EACtB,OAAO,EAAE,yBAAyB,GACjC,MAAM,IAAI,
|
|
1
|
+
{"version":3,"file":"persistence.d.ts","sourceRoot":"","sources":["../../../../../packages/session/src/lib/session/persistence.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAIvD,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,aAAa,CAAC;AAkB7D;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,YAAY,EACtB,OAAO,EAAE,yBAAyB,GACjC,MAAM,IAAI,CAmFZ"}
|
|
@@ -31,36 +31,43 @@ export function attachPersistence(internal, options) {
|
|
|
31
31
|
let timeout;
|
|
32
32
|
let isApplyingRemote = false;
|
|
33
33
|
const encoder = new TextEncoder();
|
|
34
|
+
const flushNow = () => {
|
|
35
|
+
if (timeout) {
|
|
36
|
+
clearTimeout(timeout);
|
|
37
|
+
timeout = undefined;
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
const payload = JSON.stringify(serializeSession(internal));
|
|
41
|
+
const attemptedBytes = encoder.encode(payload).byteLength;
|
|
42
|
+
if (typeof options.maxBytes === 'number'
|
|
43
|
+
&& Number.isFinite(options.maxBytes)
|
|
44
|
+
&& options.maxBytes >= 0
|
|
45
|
+
&& attemptedBytes > options.maxBytes) {
|
|
46
|
+
options.onError?.({
|
|
47
|
+
reason: 'size_limit',
|
|
48
|
+
key,
|
|
49
|
+
attemptedBytes,
|
|
50
|
+
maxBytes: options.maxBytes,
|
|
51
|
+
});
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
storage.setItem(key, payload);
|
|
55
|
+
}
|
|
56
|
+
catch (cause) {
|
|
57
|
+
options.onError?.({
|
|
58
|
+
reason: 'storage_error',
|
|
59
|
+
key,
|
|
60
|
+
cause,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
};
|
|
34
64
|
const unsubscribe = subscribeSnapshot(internal, () => {
|
|
35
65
|
if (isApplyingRemote)
|
|
36
66
|
return;
|
|
37
67
|
if (timeout)
|
|
38
68
|
clearTimeout(timeout);
|
|
39
69
|
timeout = setTimeout(() => {
|
|
40
|
-
|
|
41
|
-
const payload = JSON.stringify(serializeSession(internal));
|
|
42
|
-
const attemptedBytes = encoder.encode(payload).byteLength;
|
|
43
|
-
if (typeof options.maxBytes === 'number'
|
|
44
|
-
&& Number.isFinite(options.maxBytes)
|
|
45
|
-
&& options.maxBytes >= 0
|
|
46
|
-
&& attemptedBytes > options.maxBytes) {
|
|
47
|
-
options.onError?.({
|
|
48
|
-
reason: 'size_limit',
|
|
49
|
-
key,
|
|
50
|
-
attemptedBytes,
|
|
51
|
-
maxBytes: options.maxBytes,
|
|
52
|
-
});
|
|
53
|
-
return;
|
|
54
|
-
}
|
|
55
|
-
storage.setItem(key, payload);
|
|
56
|
-
}
|
|
57
|
-
catch (cause) {
|
|
58
|
-
options.onError?.({
|
|
59
|
-
reason: 'storage_error',
|
|
60
|
-
key,
|
|
61
|
-
cause,
|
|
62
|
-
});
|
|
63
|
-
}
|
|
70
|
+
flushNow();
|
|
64
71
|
}, 200);
|
|
65
72
|
});
|
|
66
73
|
const onStorage = (event) => {
|
|
@@ -75,26 +82,29 @@ export function attachPersistence(internal, options) {
|
|
|
75
82
|
});
|
|
76
83
|
isApplyingRemote = true;
|
|
77
84
|
replaceInternalState(internal, next);
|
|
85
|
+
isApplyingRemote = false;
|
|
78
86
|
notifySnapshotAndIssueListeners(internal);
|
|
79
87
|
}
|
|
80
88
|
catch {
|
|
81
|
-
return;
|
|
82
|
-
}
|
|
83
|
-
finally {
|
|
84
89
|
isApplyingRemote = false;
|
|
90
|
+
return;
|
|
85
91
|
}
|
|
86
92
|
};
|
|
87
93
|
const maybeAdd = globalThis.addEventListener;
|
|
88
94
|
const maybeRemove = globalThis.removeEventListener;
|
|
89
95
|
if (maybeAdd) {
|
|
90
96
|
maybeAdd('storage', onStorage);
|
|
97
|
+
maybeAdd('beforeunload', flushNow);
|
|
91
98
|
}
|
|
92
99
|
return () => {
|
|
93
|
-
if (timeout)
|
|
100
|
+
if (timeout) {
|
|
94
101
|
clearTimeout(timeout);
|
|
102
|
+
timeout = undefined;
|
|
103
|
+
}
|
|
95
104
|
unsubscribe();
|
|
96
105
|
if (maybeRemove) {
|
|
97
106
|
maybeRemove('storage', onStorage);
|
|
107
|
+
maybeRemove('beforeunload', flushNow);
|
|
98
108
|
}
|
|
99
109
|
};
|
|
100
110
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@continuum-dev/session",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"LICENSE*"
|
|
41
41
|
],
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@continuum-dev/contract": "^0.1.
|
|
44
|
-
"@continuum-dev/runtime": "^0.1.
|
|
43
|
+
"@continuum-dev/contract": "^0.1.3",
|
|
44
|
+
"@continuum-dev/runtime": "^0.1.3"
|
|
45
45
|
}
|
|
46
46
|
}
|