@openlfcp/shared-objects 0.2.0 → 0.2.2
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/dist/data-profile.d.ts +18 -1
- package/dist/data-profile.js +27 -7
- package/dist/index.d.ts +1 -1
- package/dist/listeners.d.ts +28 -0
- package/dist/listeners.js +93 -0
- package/dist/sections/data-profile.d.ts +18 -1
- package/dist/sections/data-profile.js +27 -6
- package/dist/sections/index.d.ts +1 -1
- package/package.json +3 -3
package/dist/data-profile.d.ts
CHANGED
|
@@ -84,6 +84,11 @@ export interface SharedObjectsCodec {
|
|
|
84
84
|
encode(change: CheckedChange): Uint8Array;
|
|
85
85
|
decode(plaintext: Uint8Array): CheckedChange;
|
|
86
86
|
}
|
|
87
|
+
/** An object-changed listener that threw: the error, and the change it was given. */
|
|
88
|
+
export interface SharedObjectsListenerFailure {
|
|
89
|
+
readonly error: unknown;
|
|
90
|
+
readonly change: ObjectChange;
|
|
91
|
+
}
|
|
87
92
|
export declare class SharedObjectsDataProfile {
|
|
88
93
|
#private;
|
|
89
94
|
readonly dataProfile = "org.openlfcp.shared-objects.v1";
|
|
@@ -103,8 +108,20 @@ export declare class SharedObjectsDataProfile {
|
|
|
103
108
|
static restore(checkpoint: SharedObjectsCheckpoint, options: Pick<ReplicaOptions, "resource" | "principal">): SharedObjectsDataProfile;
|
|
104
109
|
/** The current replica (a G-EP7 rebuild replaces it). */
|
|
105
110
|
get replica(): SharedObjectsReplica;
|
|
106
|
-
/**
|
|
111
|
+
/**
|
|
112
|
+
* §98, §100: object change notifications for remote merges and rebuilds.
|
|
113
|
+
* A listener that changes the model again (re-entrantly) makes the
|
|
114
|
+
* nested event reach the listeners after it before the outer one does,
|
|
115
|
+
* as in 0.2.1.
|
|
116
|
+
*/
|
|
107
117
|
onObjectChanged(listener: (change: ObjectChange) => void): () => void;
|
|
118
|
+
/**
|
|
119
|
+
* Called when an object-changed listener throws (REVIEW-20261015-03),
|
|
120
|
+
* with the error and the change. Without a handler the error goes to the
|
|
121
|
+
* runtime's reportError where there is one (a browser, Electron), else it
|
|
122
|
+
* is dropped. A handler that throws is not reported again.
|
|
123
|
+
*/
|
|
124
|
+
onListenerError(handler: (failure: SharedObjectsListenerFailure) => void): () => void;
|
|
108
125
|
/**
|
|
109
126
|
* Records which change one of this client's OWN units carries (the unit
|
|
110
127
|
* was created from a local change, so it is merged already). A later
|
package/dist/data-profile.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { LfcpError, toHex, } from "@openlfcp/core";
|
|
2
2
|
import { checkChangeActor } from "./admission/actor.js";
|
|
3
3
|
import { frameChange, frameSnapshot, unframeChange, unframeSnapshot, } from "./admission/framing.js";
|
|
4
|
+
import { GuardedListeners } from "./listeners.js";
|
|
4
5
|
import { SharedObjectsReplica } from "./replica.js";
|
|
5
6
|
import { deriveActorId, PROFILE_ID } from "./values.js";
|
|
6
7
|
export class SharedObjectsDataProfile {
|
|
@@ -12,7 +13,7 @@ export class SharedObjectsDataProfile {
|
|
|
12
13
|
#pending = new Map();
|
|
13
14
|
/** LFCP-accepted units whose actor and sequence number a different change has (§14.1), by unit ID hex. */
|
|
14
15
|
#held = new Map();
|
|
15
|
-
#listeners = new
|
|
16
|
+
#listeners = new GuardedListeners((error, change) => Object.freeze({ error, change }));
|
|
16
17
|
constructor(replica) {
|
|
17
18
|
this.#replica = replica;
|
|
18
19
|
}
|
|
@@ -55,15 +56,34 @@ export class SharedObjectsDataProfile {
|
|
|
55
56
|
get replica() {
|
|
56
57
|
return this.#replica;
|
|
57
58
|
}
|
|
58
|
-
/**
|
|
59
|
+
/**
|
|
60
|
+
* §98, §100: object change notifications for remote merges and rebuilds.
|
|
61
|
+
* A listener that changes the model again (re-entrantly) makes the
|
|
62
|
+
* nested event reach the listeners after it before the outer one does,
|
|
63
|
+
* as in 0.2.1.
|
|
64
|
+
*/
|
|
59
65
|
onObjectChanged(listener) {
|
|
60
|
-
this.#listeners.add(listener);
|
|
61
|
-
return () => this.#listeners.delete(listener);
|
|
66
|
+
return this.#listeners.add(listener);
|
|
62
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* Called when an object-changed listener throws (REVIEW-20261015-03),
|
|
70
|
+
* with the error and the change. Without a handler the error goes to the
|
|
71
|
+
* runtime's reportError where there is one (a browser, Electron), else it
|
|
72
|
+
* is dropped. A handler that throws is not reported again.
|
|
73
|
+
*/
|
|
74
|
+
onListenerError(handler) {
|
|
75
|
+
return this.#listeners.onError(handler);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Every listener gets every change, and a listener that throws never
|
|
79
|
+
* turns the operation that changed the model (a merge, a rebuild, a
|
|
80
|
+
* loaded Snapshot, a reset) into a failure: the change stands and the
|
|
81
|
+
* error is reported (onListenerError), never recursively. An engine trap
|
|
82
|
+
* is thrown on once every listener has had every change, as SyncClient
|
|
83
|
+
* does: the engine is dead.
|
|
84
|
+
*/
|
|
63
85
|
#emit(changes) {
|
|
64
|
-
|
|
65
|
-
for (const l of this.#listeners)
|
|
66
|
-
l(c);
|
|
86
|
+
this.#listeners.emit(changes);
|
|
67
87
|
}
|
|
68
88
|
/**
|
|
69
89
|
* Records which change one of this client's OWN units carries (the unit
|
package/dist/index.d.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
export declare const PACKAGE = "@openlfcp/shared-objects";
|
|
7
7
|
export { type CheckedChange, checkChange, checkSaveHeader, frameChange, frameSnapshot, unframeChange, unframeSnapshot, } from "./admission/framing.js";
|
|
8
8
|
export { CHANGE_LIMITS, type ChunkExpansion, checkChangeExpansion, checkSnapshotExpansion, MAX_DOCUMENT_DEPTH, SNAPSHOT_LIMITS_FLOOR, type SnapshotLimits, } from "./admission/limits.js";
|
|
9
|
-
export { type SharedObjectsApplyResult, type SharedObjectsBatchResult, type SharedObjectsCheckpoint, type SharedObjectsCodec, SharedObjectsDataProfile, type SharedObjectsDiagnostic, type SharedObjectsExcludeResult, type SharedObjectsSnapshotCodec, type SharedObjectsUnit, } from "./data-profile.js";
|
|
9
|
+
export { type SharedObjectsApplyResult, type SharedObjectsBatchResult, type SharedObjectsCheckpoint, type SharedObjectsCodec, SharedObjectsDataProfile, type SharedObjectsDiagnostic, type SharedObjectsExcludeResult, type SharedObjectsListenerFailure, type SharedObjectsSnapshotCodec, type SharedObjectsUnit, } from "./data-profile.js";
|
|
10
10
|
export { applyBatchChecked, applyChecked, type BatchReceiveResult, type BuiltReplica, isWasmTrap, type LocalChange, type ObjectChange, ObjectIdCollisionError, type ObjectStatus, type ReceiveResult, type ReplicaIntent, type ReplicaOptions, type ReplicaValidation, type ResolveFieldConflict, resolveFieldConflict, SCALAR_FIELDS, type ScalarField, type ScalarView, SharedObjectsReplica, type TaskView, } from "./replica.js";
|
|
11
11
|
export { addTag, assign, cancel, clearDue, clearScheduled, complete, createTask, deleteTask, type NewTask, type ParsedTask, ProfileError, parseTask, removeTag, reopen, restoreTask, setDue, setPriority, setScheduled, setStatus, setTitle, type Task, type TaskChange, type TaskIntent, type TaskPriority, type TaskStatus, taskToJson, unassign, } from "./task.js";
|
|
12
12
|
export { DIAGNOSTIC_ORDER, firstPerField, isMap, type Json, objectProblems, type ProfileDiagnostic, ProfileInvalidError, type ProfileProblem, pointerToken, type RootValidation, validateRoot, validateTransition, } from "./validate.js";
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Guarded change notifications for the Data Profile handlers
|
|
3
|
+
* (REVIEW-20261015-03): every listener gets each event, and a listener that
|
|
4
|
+
* throws never turns the operation that changed the model into a failure.
|
|
5
|
+
* Its error goes to the handlers registered with onError, or, without one,
|
|
6
|
+
* to the runtime's reportError where there is one (a browser, Electron);
|
|
7
|
+
* else it is dropped. A handler that throws, or a failure while one is
|
|
8
|
+
* being reported, is not reported again (no recursion). An engine trap is
|
|
9
|
+
* the exception: it is thrown on once every listener has had the event, as
|
|
10
|
+
* the client's SyncClient does, since the engine is dead.
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* A thrown value that looks like an engine trap (WebAssembly RuntimeError, a
|
|
14
|
+
* terminated module): the rule of the client's isEngineTrap, which this
|
|
15
|
+
* package cannot import.
|
|
16
|
+
*/
|
|
17
|
+
export declare function isEngineTrap(e: unknown): boolean;
|
|
18
|
+
/** Listeners of events `E`, and the handlers of their failures `F`. */
|
|
19
|
+
export declare class GuardedListeners<E, F> {
|
|
20
|
+
#private;
|
|
21
|
+
/** `failure` builds what a handler receives from the error and the event. */
|
|
22
|
+
constructor(failure: (error: unknown, event: E) => F);
|
|
23
|
+
add(listener: (event: E) => void): () => void;
|
|
24
|
+
onError(handler: (failure: F) => void): () => void;
|
|
25
|
+
/** Delivers each event to every listener; see the module comment. */
|
|
26
|
+
emit(events: readonly E[]): void;
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=listeners.d.ts.map
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Guarded change notifications for the Data Profile handlers
|
|
3
|
+
* (REVIEW-20261015-03): every listener gets each event, and a listener that
|
|
4
|
+
* throws never turns the operation that changed the model into a failure.
|
|
5
|
+
* Its error goes to the handlers registered with onError, or, without one,
|
|
6
|
+
* to the runtime's reportError where there is one (a browser, Electron);
|
|
7
|
+
* else it is dropped. A handler that throws, or a failure while one is
|
|
8
|
+
* being reported, is not reported again (no recursion). An engine trap is
|
|
9
|
+
* the exception: it is thrown on once every listener has had the event, as
|
|
10
|
+
* the client's SyncClient does, since the engine is dead.
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* A thrown value that looks like an engine trap (WebAssembly RuntimeError, a
|
|
14
|
+
* terminated module): the rule of the client's isEngineTrap, which this
|
|
15
|
+
* package cannot import.
|
|
16
|
+
*/
|
|
17
|
+
export function isEngineTrap(e) {
|
|
18
|
+
const wasm = globalThis
|
|
19
|
+
.WebAssembly?.RuntimeError;
|
|
20
|
+
if (wasm !== undefined && e instanceof wasm)
|
|
21
|
+
return true;
|
|
22
|
+
if (!(e instanceof Error))
|
|
23
|
+
return false;
|
|
24
|
+
return (e.name === "RuntimeError" ||
|
|
25
|
+
/\b(module|instance)\b.*\bterminated\b|\bunreachable\b executed|\bwasm trap\b/i.test(e.message));
|
|
26
|
+
}
|
|
27
|
+
/** Listeners of events `E`, and the handlers of their failures `F`. */
|
|
28
|
+
export class GuardedListeners {
|
|
29
|
+
#listeners = new Set();
|
|
30
|
+
#errorHandlers = new Set();
|
|
31
|
+
#failure;
|
|
32
|
+
#reporting = false;
|
|
33
|
+
/** `failure` builds what a handler receives from the error and the event. */
|
|
34
|
+
constructor(failure) {
|
|
35
|
+
this.#failure = failure;
|
|
36
|
+
}
|
|
37
|
+
add(listener) {
|
|
38
|
+
this.#listeners.add(listener);
|
|
39
|
+
return () => this.#listeners.delete(listener);
|
|
40
|
+
}
|
|
41
|
+
onError(handler) {
|
|
42
|
+
this.#errorHandlers.add(handler);
|
|
43
|
+
return () => this.#errorHandlers.delete(handler);
|
|
44
|
+
}
|
|
45
|
+
/** Delivers each event to every listener; see the module comment. */
|
|
46
|
+
emit(events) {
|
|
47
|
+
let trap;
|
|
48
|
+
const failed = [];
|
|
49
|
+
for (const event of events)
|
|
50
|
+
for (const l of [...this.#listeners])
|
|
51
|
+
try {
|
|
52
|
+
l(event);
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
if (isEngineTrap(error))
|
|
56
|
+
trap ??= { error };
|
|
57
|
+
else
|
|
58
|
+
failed.push({ error, event });
|
|
59
|
+
}
|
|
60
|
+
for (const { error, event } of failed)
|
|
61
|
+
this.#report(error, event);
|
|
62
|
+
if (trap !== undefined)
|
|
63
|
+
throw trap.error;
|
|
64
|
+
}
|
|
65
|
+
#report(error, event) {
|
|
66
|
+
if (this.#reporting)
|
|
67
|
+
return;
|
|
68
|
+
this.#reporting = true;
|
|
69
|
+
try {
|
|
70
|
+
if (this.#errorHandlers.size === 0) {
|
|
71
|
+
const report = globalThis.reportError;
|
|
72
|
+
if (typeof report === "function")
|
|
73
|
+
report(error);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const failure = this.#failure(error, event);
|
|
77
|
+
for (const h of [...this.#errorHandlers])
|
|
78
|
+
try {
|
|
79
|
+
h(failure);
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
// A failing error handler is not reported again (no recursion).
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
// reportError itself failed: nothing left to report to.
|
|
87
|
+
}
|
|
88
|
+
finally {
|
|
89
|
+
this.#reporting = false;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=listeners.js.map
|
|
@@ -105,14 +105,31 @@ export interface SectionsNodesChanged {
|
|
|
105
105
|
readonly origin: "local" | "remote" | "rebuild";
|
|
106
106
|
readonly modelRevision: string;
|
|
107
107
|
}
|
|
108
|
+
/** A nodes-changed listener that threw: the error, and the event it was given. */
|
|
109
|
+
export interface SectionsListenerFailure {
|
|
110
|
+
readonly error: unknown;
|
|
111
|
+
readonly event: SectionsNodesChanged;
|
|
112
|
+
}
|
|
108
113
|
export declare class SharedSectionsDataProfile {
|
|
109
114
|
#private;
|
|
110
115
|
readonly dataProfile = "org.openlfcp.shared-sections.v1";
|
|
111
116
|
constructor(replica: SectionReplica);
|
|
112
117
|
/** The current replica (a rebuild replaces it). */
|
|
113
118
|
get replica(): SectionReplica;
|
|
114
|
-
/**
|
|
119
|
+
/**
|
|
120
|
+
* §5 nodes-changed for remote merges, rebuilds and recorded local changes.
|
|
121
|
+
* A listener that changes the model again (re-entrantly) makes the
|
|
122
|
+
* nested event reach the listeners after it before the outer one does,
|
|
123
|
+
* as in 0.2.1.
|
|
124
|
+
*/
|
|
115
125
|
onNodesChanged(listener: (event: SectionsNodesChanged) => void): () => void;
|
|
126
|
+
/**
|
|
127
|
+
* Called when a nodes-changed listener throws (REVIEW-20261015-03), with
|
|
128
|
+
* the error and the event. Without a handler the error goes to the
|
|
129
|
+
* runtime's reportError where there is one (a browser, Electron), else it
|
|
130
|
+
* is dropped. A handler that throws is not reported again.
|
|
131
|
+
*/
|
|
132
|
+
onListenerError(handler: (failure: SectionsListenerFailure) => void): () => void;
|
|
116
133
|
/** The codec of one unit, bound to its signer's section actor (§2). */
|
|
117
134
|
codecFor(unit: {
|
|
118
135
|
readonly resourceId: ResourceId;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { LfcpError, toHex, } from "@openlfcp/core";
|
|
2
2
|
import { checkChangeActor } from "../admission/actor.js";
|
|
3
3
|
import { checkChange, frameChange, frameSnapshot, unframeChange, unframeSnapshot, } from "../admission/framing.js";
|
|
4
|
+
import { GuardedListeners } from "../listeners.js";
|
|
4
5
|
import { SectionReplica } from "./replica.js";
|
|
5
6
|
import { deriveSectionActorId, SECTIONS_PROFILE_ID } from "./values.js";
|
|
6
7
|
/** Each node's (and the title's) projected state, to tell which ones a merge changed. */
|
|
@@ -62,7 +63,7 @@ export class SharedSectionsDataProfile {
|
|
|
62
63
|
#pending = new Map();
|
|
63
64
|
/** LFCP-accepted units whose actor sequence a different change has (SOP §14.1), by unit ID hex. */
|
|
64
65
|
#held = new Map();
|
|
65
|
-
#listeners = new
|
|
66
|
+
#listeners = new GuardedListeners((error, event) => Object.freeze({ error, event }));
|
|
66
67
|
constructor(replica) {
|
|
67
68
|
this.#replica = replica;
|
|
68
69
|
}
|
|
@@ -70,17 +71,37 @@ export class SharedSectionsDataProfile {
|
|
|
70
71
|
get replica() {
|
|
71
72
|
return this.#replica;
|
|
72
73
|
}
|
|
73
|
-
/**
|
|
74
|
+
/**
|
|
75
|
+
* §5 nodes-changed for remote merges, rebuilds and recorded local changes.
|
|
76
|
+
* A listener that changes the model again (re-entrantly) makes the
|
|
77
|
+
* nested event reach the listeners after it before the outer one does,
|
|
78
|
+
* as in 0.2.1.
|
|
79
|
+
*/
|
|
74
80
|
onNodesChanged(listener) {
|
|
75
|
-
this.#listeners.add(listener);
|
|
76
|
-
return () => this.#listeners.delete(listener);
|
|
81
|
+
return this.#listeners.add(listener);
|
|
77
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* Called when a nodes-changed listener throws (REVIEW-20261015-03), with
|
|
85
|
+
* the error and the event. Without a handler the error goes to the
|
|
86
|
+
* runtime's reportError where there is one (a browser, Electron), else it
|
|
87
|
+
* is dropped. A handler that throws is not reported again.
|
|
88
|
+
*/
|
|
89
|
+
onListenerError(handler) {
|
|
90
|
+
return this.#listeners.onError(handler);
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Every listener gets the event, and a listener that throws never turns
|
|
94
|
+
* the operation that changed the model (a merge, a recorded local change,
|
|
95
|
+
* a rebuild, a loaded Snapshot) into a failure: the change stands and the
|
|
96
|
+
* error is reported (onListenerError), never recursively. An engine trap
|
|
97
|
+
* is the exception and is thrown on once every listener has had the
|
|
98
|
+
* event, as before and as SyncClient does: the engine is dead.
|
|
99
|
+
*/
|
|
78
100
|
#emit(nodeIds, origin) {
|
|
79
101
|
if (nodeIds.length === 0)
|
|
80
102
|
return;
|
|
81
103
|
const event = Object.freeze({ nodeIds, origin, modelRevision: this.#replica.revision() });
|
|
82
|
-
|
|
83
|
-
l(event);
|
|
104
|
+
this.#listeners.emit([event]);
|
|
84
105
|
}
|
|
85
106
|
/** The codec of one unit, bound to its signer's section actor (§2). */
|
|
86
107
|
codecFor(unit) {
|
package/dist/sections/index.d.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* shared `@openlfcp/shared-objects/admission` with SECTIONS_ACTOR_DOMAIN.
|
|
9
9
|
*/
|
|
10
10
|
export { ADMISSION_ORDER, type AdmissionDiagnostic, SectionAdmissionError, } from "./admission.js";
|
|
11
|
-
export { type SectionsApplyResult, type SectionsBatchResult, type SectionsCheckpoint, type SectionsCodec, type SectionsCommitBinding, type SectionsExcludeResult, type SectionsNodesChanged, type SectionsSnapshotBinding, type SectionsSnapshotCodec, type SectionsStagedOperation, type SectionsUnit, SharedSectionsDataProfile, } from "./data-profile.js";
|
|
11
|
+
export { type SectionsApplyResult, type SectionsBatchResult, type SectionsCheckpoint, type SectionsCodec, type SectionsCommitBinding, type SectionsExcludeResult, type SectionsListenerFailure, type SectionsNodesChanged, type SectionsSnapshotBinding, type SectionsSnapshotCodec, type SectionsStagedOperation, type SectionsUnit, SharedSectionsDataProfile, } from "./data-profile.js";
|
|
12
12
|
export { type ProfileModel, profileModel, SUPPORTED_DATA_PROFILES, taskRefModel, } from "./dispatch.js";
|
|
13
13
|
export { SectionDocument, type ValueType } from "./document.js";
|
|
14
14
|
export { AUTHORING_BUDGET, type SectionIntent, type SectionIntentCode, SectionIntentError, type SectionLocalChange, type SectionReceiveResult, type SectionRefusal, SectionReplica, type SectionReplicaOptions, type SectionSnapshot, type SectionSnapshotNode, type SectionUnit, type StagedSectionChange, type TextEdit, } from "./replica.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openlfcp/shared-objects",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "SHARED-OBJECTS-PROFILE-01 (org.openlfcp.shared-objects.v1) implementation.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"openlfcp",
|
|
@@ -53,8 +53,8 @@
|
|
|
53
53
|
"dependencies": {
|
|
54
54
|
"@automerge/automerge": "3.5.0",
|
|
55
55
|
"fflate": "0.8.3",
|
|
56
|
-
"@openlfcp/core": "^0.2.
|
|
57
|
-
"@openlfcp/crypto": "^0.2.
|
|
56
|
+
"@openlfcp/core": "^0.2.2",
|
|
57
|
+
"@openlfcp/crypto": "^0.2.2"
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
60
60
|
"fast-check": "4.10.2"
|