@kyneta/changefeed 1.3.1 → 1.5.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/dist/index.d.ts +44 -42
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +196 -125
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
//#region src/change.d.ts
|
|
1
2
|
/**
|
|
2
3
|
* All changes carry a string `type` discriminant. Built-in change types
|
|
3
4
|
* use well-known strings ("text", "sequence", "map", "replace", "tree").
|
|
@@ -8,9 +9,10 @@
|
|
|
8
9
|
* `Changeset` in `changefeed.ts`.
|
|
9
10
|
*/
|
|
10
11
|
interface ChangeBase {
|
|
11
|
-
|
|
12
|
+
readonly type: string;
|
|
12
13
|
}
|
|
13
|
-
|
|
14
|
+
//#endregion
|
|
15
|
+
//#region src/changefeed.d.ts
|
|
14
16
|
/**
|
|
15
17
|
* The single symbol that marks a value as a changefeed. Accessing
|
|
16
18
|
* `obj[CHANGEFEED]` yields a `ChangefeedProtocol<S, C>` — the current
|
|
@@ -34,10 +36,10 @@ declare const CHANGEFEED: unique symbol;
|
|
|
34
36
|
* regardless of how the changes were produced.
|
|
35
37
|
*/
|
|
36
38
|
interface Changeset<C = ChangeBase> {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
/** The individual changes in this batch. */
|
|
40
|
+
readonly changes: readonly C[];
|
|
41
|
+
/** Provenance of the batch (e.g. "sync", "undo", "local"). */
|
|
42
|
+
readonly origin?: string;
|
|
41
43
|
}
|
|
42
44
|
/**
|
|
43
45
|
* The protocol object that sits behind the `[CHANGEFEED]` symbol.
|
|
@@ -60,10 +62,10 @@ interface Changeset<C = ChangeBase> {
|
|
|
60
62
|
* and `.subscribe()` in one interface).
|
|
61
63
|
*/
|
|
62
64
|
interface ChangefeedProtocol<S, C extends ChangeBase = ChangeBase> {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
/** The current value, always live (a getter). */
|
|
66
|
+
readonly current: S;
|
|
67
|
+
/** Subscribe to future changes. Returns an unsubscribe function. */
|
|
68
|
+
subscribe(callback: (changeset: Changeset<C>) => void): () => void;
|
|
67
69
|
}
|
|
68
70
|
/**
|
|
69
71
|
* The developer-facing changefeed type: a reactive value with direct
|
|
@@ -80,12 +82,12 @@ interface ChangefeedProtocol<S, C extends ChangeBase = ChangeBase> {
|
|
|
80
82
|
* `Changefeed`, or `createChangefeed()` to build one from scratch.
|
|
81
83
|
*/
|
|
82
84
|
interface Changefeed<S, C extends ChangeBase = ChangeBase> {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
85
|
+
/** The protocol object behind the symbol. */
|
|
86
|
+
readonly [CHANGEFEED]: ChangefeedProtocol<S, C>;
|
|
87
|
+
/** The current value, always live (a getter). */
|
|
88
|
+
readonly current: S;
|
|
89
|
+
/** Subscribe to future changes. Returns an unsubscribe function. */
|
|
90
|
+
subscribe(callback: (changeset: Changeset<C>) => void): () => void;
|
|
89
91
|
}
|
|
90
92
|
/**
|
|
91
93
|
* An object that carries a changefeed protocol under the `[CHANGEFEED]`
|
|
@@ -95,7 +97,7 @@ interface Changefeed<S, C extends ChangeBase = ChangeBase> {
|
|
|
95
97
|
* interface to participate in the reactive protocol.
|
|
96
98
|
*/
|
|
97
99
|
interface HasChangefeed<S = unknown, A extends ChangeBase = ChangeBase> {
|
|
98
|
-
|
|
100
|
+
readonly [CHANGEFEED]: ChangefeedProtocol<S, A>;
|
|
99
101
|
}
|
|
100
102
|
/**
|
|
101
103
|
* Returns `true` if `value` has a `[CHANGEFEED]` property, i.e. it
|
|
@@ -137,7 +139,8 @@ declare function changefeed<S, C extends ChangeBase>(source: HasChangefeed<S, C>
|
|
|
137
139
|
* ```
|
|
138
140
|
*/
|
|
139
141
|
declare function createChangefeed<S, C extends ChangeBase = ChangeBase>(getCurrent: () => S): [feed: Changefeed<S, C>, emit: (changeset: Changeset<C>) => void];
|
|
140
|
-
|
|
142
|
+
//#endregion
|
|
143
|
+
//#region src/callable.d.ts
|
|
141
144
|
/**
|
|
142
145
|
* A changefeed that is also callable — `feed()` returns `feed.current`.
|
|
143
146
|
*
|
|
@@ -164,7 +167,8 @@ type CallableChangefeed<S, C extends ChangeBase = ChangeBase> = Changefeed<S, C>
|
|
|
164
167
|
* ```
|
|
165
168
|
*/
|
|
166
169
|
declare function createCallable<S, C extends ChangeBase>(feed: Changefeed<S, C>): CallableChangefeed<S, C>;
|
|
167
|
-
|
|
170
|
+
//#endregion
|
|
171
|
+
//#region src/reactive-map.d.ts
|
|
168
172
|
/**
|
|
169
173
|
* A callable changefeed over a `ReadonlyMap<K, V>` with lifted
|
|
170
174
|
* collection accessors.
|
|
@@ -177,16 +181,16 @@ declare function createCallable<S, C extends ChangeBase>(feed: Changefeed<S, C>)
|
|
|
177
181
|
* `CallableChangefeed<ReadonlyMap<K, V>, C>` or `Changefeed` is expected.
|
|
178
182
|
*/
|
|
179
183
|
interface ReactiveMap<K, V, C extends ChangeBase = ChangeBase> extends CallableChangefeed<ReadonlyMap<K, V>, C> {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
184
|
+
/** Get the value for a key, or `undefined` if absent. */
|
|
185
|
+
get(key: K): V | undefined;
|
|
186
|
+
/** Whether the map contains a key. */
|
|
187
|
+
has(key: K): boolean;
|
|
188
|
+
/** An iterator over all keys. */
|
|
189
|
+
keys(): IterableIterator<K>;
|
|
190
|
+
/** The number of entries. */
|
|
191
|
+
readonly size: number;
|
|
192
|
+
/** Iterate over `[key, value]` pairs. */
|
|
193
|
+
[Symbol.iterator](): IterableIterator<[K, V]>;
|
|
190
194
|
}
|
|
191
195
|
/**
|
|
192
196
|
* The producer-side handle for a `ReactiveMap`.
|
|
@@ -199,14 +203,14 @@ interface ReactiveMap<K, V, C extends ChangeBase = ChangeBase> extends CallableC
|
|
|
199
203
|
* changeset — e.g. `clear()` → N × `set()` → one `emit()`.
|
|
200
204
|
*/
|
|
201
205
|
interface ReactiveMapHandle<K, V, C extends ChangeBase> {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
206
|
+
/** Insert or overwrite an entry. Does NOT emit. */
|
|
207
|
+
set(key: K, value: V): void;
|
|
208
|
+
/** Remove an entry. Returns `true` if the key was present. Does NOT emit. */
|
|
209
|
+
delete(key: K): boolean;
|
|
210
|
+
/** Remove all entries. Does NOT emit. */
|
|
211
|
+
clear(): void;
|
|
212
|
+
/** Push a changeset to all subscribers. */
|
|
213
|
+
emit(changeset: Changeset<C>): void;
|
|
210
214
|
}
|
|
211
215
|
/**
|
|
212
216
|
* Create a `ReactiveMap<K, V, C>` and its producer-side handle.
|
|
@@ -227,9 +231,7 @@ interface ReactiveMapHandle<K, V, C extends ChangeBase> {
|
|
|
227
231
|
* peers.size // 1
|
|
228
232
|
* ```
|
|
229
233
|
*/
|
|
230
|
-
declare function createReactiveMap<K, V, C extends ChangeBase = ChangeBase>(): [
|
|
231
|
-
|
|
232
|
-
ReactiveMapHandle<K, V, C>
|
|
233
|
-
];
|
|
234
|
-
|
|
234
|
+
declare function createReactiveMap<K, V, C extends ChangeBase = ChangeBase>(): [ReactiveMap<K, V, C>, ReactiveMapHandle<K, V, C>];
|
|
235
|
+
//#endregion
|
|
235
236
|
export { CHANGEFEED, type CallableChangefeed, type ChangeBase, type Changefeed, type ChangefeedProtocol, type Changeset, type HasChangefeed, type ReactiveMap, type ReactiveMapHandle, changefeed, createCallable, createChangefeed, createReactiveMap, hasChangefeed, staticChangefeed };
|
|
237
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/change.ts","../src/changefeed.ts","../src/callable.ts","../src/reactive-map.ts"],"mappings":";;AAyBA;;;;;;;;UAAiB,UAAA;EAAA,SACN,IAAA;AAAA;;;AADX;;;;;;;;AAAA,cCMa,UAAA;;;;;AAmBb;;;;;;;;;UAAiB,SAAA,KAAc,UAAA;EAId;EAAA,SAFN,OAAA,WAAkB,CAAA;EA6BM;EAAA,SA3BxB,MAAA;AAAA;;;;;;;;;;;;;;;;;;;;AAoDX;UAzBiB,kBAAA,cAAgC,UAAA,GAAa,UAAA;EAyBnC;EAAA,SAvBhB,OAAA,EAAS,CAAA;EAuBkC;EArBpD,SAAA,CAAU,QAAA,GAAW,SAAA,EAAW,SAAA,CAAU,CAAA;AAAA;;;;;;;;;;;;;;;UAqB3B,UAAA,cAAwB,UAAA,GAAa,UAAA;EAI3C;EAAA,UAFC,UAAA,GAAa,kBAAA,CAAmB,CAAA,EAAG,CAAA;EAI7C;EAAA,SAFS,OAAA,EAAS,CAAA;EAEwB;EAA1C,SAAA,CAAU,QAAA,GAAW,SAAA,EAAW,SAAA,CAAU,CAAA;AAAA;;;AAU5C;;;;;UAAiB,aAAA,wBAAqC,UAAA,GAAa,UAAA;EAAA,UACvD,UAAA,GAAa,kBAAA,CAAmB,CAAA,EAAG,CAAA;AAAA;;;;;iBAW/B,aAAA,wBAAqC,UAAA,GAAa,UAAA,CAAA,CAChE,KAAA,YACC,KAAA,IAAS,aAAA,CAAc,CAAA,EAAG,CAAA;;;;;;iBAkBb,gBAAA,GAAA,CAAoB,IAAA,EAAM,CAAA,GAAI,kBAAA,CAAmB,CAAA;;;AApBjE;;;;;;;;;;iBA+CgB,UAAA,cAAwB,UAAA,CAAA,CACtC,MAAA,EAAQ,aAAA,CAAc,CAAA,EAAG,CAAA,IACxB,UAAA,CAAW,CAAA,EAAG,CAAA;;;;;;;;;;;AA7BjB;;;;;iBA6DgB,gBAAA,cAA8B,UAAA,GAAa,UAAA,CAAA,CACzD,UAAA,QAAkB,CAAA,IAChB,IAAA,EAAM,UAAA,CAAW,CAAA,EAAG,CAAA,GAAI,IAAA,GAAO,SAAA,EAAW,SAAA,CAAU,CAAA;;;;;;;;;KClM5C,kBAAA,cAEA,UAAA,GAAa,UAAA,IACrB,UAAA,CAAW,CAAA,EAAG,CAAA,WAAY,CAAA;ADK9B;;;;;AAmBA;;;;;;;;;;;AA+BA;;AAlDA,iBCmBgB,cAAA,cAA4B,UAAA,CAAA,CAC1C,IAAA,EAAM,UAAA,CAAW,CAAA,EAAG,CAAA,IACnB,kBAAA,CAAmB,CAAA,EAAG,CAAA;;;;;;;;;ADrBzB;;;;;UEAiB,WAAA,iBAA4B,UAAA,GAAa,UAAA,UAChD,kBAAA,CAAmB,WAAA,CAAY,CAAA,EAAG,CAAA,GAAI,CAAA;EFkBtB;EEhBxB,GAAA,CAAI,GAAA,EAAK,CAAA,GAAI,CAAA;EFkBe;EEhB5B,GAAA,CAAI,GAAA,EAAK,CAAA;EFcoB;EEZ7B,IAAA,IAAQ,gBAAA,CAAiB,CAAA;EFcE;EAAA,SEZlB,IAAA;EFcM;EAAA,CEZd,MAAA,CAAO,QAAP,KAAoB,gBAAA,EAAkB,CAAA,EAAG,CAAA;AAAA;;;;;;;;;;;UAa3B,iBAAA,iBAAkC,UAAA;EF0BF;EExB/C,GAAA,CAAI,GAAA,EAAK,CAAA,EAAG,KAAA,EAAO,CAAA;EF0BV;EExBT,MAAA,CAAO,GAAA,EAAK,CAAA;EF0BZ;EExBA,KAAA;EFwB0C;EEtB1C,IAAA,CAAK,SAAA,EAAW,SAAA,CAAU,CAAA;AAAA;;;AF2C5B;;;;;;;;;;;;;;;;;iBEjBgB,iBAAA,iBAAkC,UAAA,GAAa,UAAA,CAAA,CAAA,IAC7D,WAAA,CAAY,CAAA,EAAG,CAAA,EAAG,CAAA,GAClB,iBAAA,CAAkB,CAAA,EAAG,CAAA,EAAG,CAAA"}
|
package/dist/index.js
CHANGED
|
@@ -1,138 +1,209 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
//#region src/changefeed.ts
|
|
2
|
+
/**
|
|
3
|
+
* The single symbol that marks a value as a changefeed. Accessing
|
|
4
|
+
* `obj[CHANGEFEED]` yields a `ChangefeedProtocol<S, C>` — the current
|
|
5
|
+
* value and a stream of future changes.
|
|
6
|
+
*
|
|
7
|
+
* Uses `Symbol.for` so that multiple copies of this module (e.g. in
|
|
8
|
+
* different bundle chunks) share the same symbol identity.
|
|
9
|
+
*/
|
|
10
|
+
const CHANGEFEED = Symbol.for("kyneta:changefeed");
|
|
11
|
+
/**
|
|
12
|
+
* Returns `true` if `value` has a `[CHANGEFEED]` property, i.e. it
|
|
13
|
+
* implements the `HasChangefeed` interface.
|
|
14
|
+
*/
|
|
3
15
|
function hasChangefeed(value) {
|
|
4
|
-
|
|
16
|
+
return value !== null && value !== void 0 && (typeof value === "object" || typeof value === "function") && CHANGEFEED in value;
|
|
5
17
|
}
|
|
18
|
+
/**
|
|
19
|
+
* Creates a changefeed protocol that never emits changes — useful for
|
|
20
|
+
* static/non-reactive data sources that still need to participate in
|
|
21
|
+
* the changefeed protocol.
|
|
22
|
+
*/
|
|
6
23
|
function staticChangefeed(head) {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
};
|
|
24
|
+
return {
|
|
25
|
+
get current() {
|
|
26
|
+
return head;
|
|
27
|
+
},
|
|
28
|
+
subscribe() {
|
|
29
|
+
return () => {};
|
|
30
|
+
}
|
|
31
|
+
};
|
|
16
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* Project any object with `[CHANGEFEED]` into a developer-facing
|
|
35
|
+
* `Changefeed<S, C>` — lifting the hidden protocol surface to direct
|
|
36
|
+
* `.current` and `.subscribe()` accessibility.
|
|
37
|
+
*
|
|
38
|
+
* ```ts
|
|
39
|
+
* const feed = changefeed(doc.title)
|
|
40
|
+
* feed.current // live value
|
|
41
|
+
* feed.subscribe(cb) // subscribe to changes
|
|
42
|
+
* feed[CHANGEFEED] // the protocol object (same as doc.title[CHANGEFEED])
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
17
45
|
function changefeed(source) {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
46
|
+
const protocol = source[CHANGEFEED];
|
|
47
|
+
return {
|
|
48
|
+
[CHANGEFEED]: protocol,
|
|
49
|
+
get current() {
|
|
50
|
+
return protocol.current;
|
|
51
|
+
},
|
|
52
|
+
subscribe(callback) {
|
|
53
|
+
return protocol.subscribe(callback);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
28
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* Create a standalone `Changefeed<S, C>` with push semantics.
|
|
59
|
+
*
|
|
60
|
+
* Returns a tuple of the feed and an emit function. The feed's
|
|
61
|
+
* `[CHANGEFEED]` returns the protocol view of itself. Manages its
|
|
62
|
+
* own subscriber set internally.
|
|
63
|
+
*
|
|
64
|
+
* ```ts
|
|
65
|
+
* const [feed, emit] = createChangefeed(() => count)
|
|
66
|
+
* feed.current // read live value
|
|
67
|
+
* feed.subscribe(cs => { ... }) // subscribe
|
|
68
|
+
* hasChangefeed(feed) // true
|
|
69
|
+
* emit({ changes: [{ type: "replace", value: 42 }] }) // push
|
|
70
|
+
* ```
|
|
71
|
+
*/
|
|
29
72
|
function createChangefeed(getCurrent) {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
};
|
|
56
|
-
return [feed, emit];
|
|
73
|
+
const subscribers = /* @__PURE__ */ new Set();
|
|
74
|
+
const protocol = {
|
|
75
|
+
get current() {
|
|
76
|
+
return getCurrent();
|
|
77
|
+
},
|
|
78
|
+
subscribe(callback) {
|
|
79
|
+
subscribers.add(callback);
|
|
80
|
+
return () => {
|
|
81
|
+
subscribers.delete(callback);
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
const feed = {
|
|
86
|
+
[CHANGEFEED]: protocol,
|
|
87
|
+
get current() {
|
|
88
|
+
return getCurrent();
|
|
89
|
+
},
|
|
90
|
+
subscribe(callback) {
|
|
91
|
+
return protocol.subscribe(callback);
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
const emit = (changeset) => {
|
|
95
|
+
for (const cb of subscribers) cb(changeset);
|
|
96
|
+
};
|
|
97
|
+
return [feed, emit];
|
|
57
98
|
}
|
|
58
|
-
|
|
59
|
-
|
|
99
|
+
//#endregion
|
|
100
|
+
//#region src/callable.ts
|
|
101
|
+
/**
|
|
102
|
+
* Wrap a `Changefeed<S, C>` in a callable function-object.
|
|
103
|
+
*
|
|
104
|
+
* The returned object:
|
|
105
|
+
* - `feed()` → `feed.current` (callable)
|
|
106
|
+
* - `feed.current` → delegated getter
|
|
107
|
+
* - `feed.subscribe(cb)` → delegated
|
|
108
|
+
* - `feed[CHANGEFEED]` → delegated protocol
|
|
109
|
+
* - `hasChangefeed(feed)` → `true`
|
|
110
|
+
*
|
|
111
|
+
* ```ts
|
|
112
|
+
* const [source, emit] = createChangefeed(() => count)
|
|
113
|
+
* const feed = createCallable(source)
|
|
114
|
+
* feed() // read current value
|
|
115
|
+
* feed.current // same as feed()
|
|
116
|
+
* feed.subscribe(cb) // subscribe to changes
|
|
117
|
+
* ```
|
|
118
|
+
*/
|
|
60
119
|
function createCallable(feed) {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
120
|
+
const callable = () => feed.current;
|
|
121
|
+
Object.defineProperty(callable, CHANGEFEED, {
|
|
122
|
+
get() {
|
|
123
|
+
return feed[CHANGEFEED];
|
|
124
|
+
},
|
|
125
|
+
enumerable: false,
|
|
126
|
+
configurable: false
|
|
127
|
+
});
|
|
128
|
+
Object.defineProperty(callable, "current", {
|
|
129
|
+
get() {
|
|
130
|
+
return feed.current;
|
|
131
|
+
},
|
|
132
|
+
enumerable: true,
|
|
133
|
+
configurable: false
|
|
134
|
+
});
|
|
135
|
+
callable.subscribe = (callback) => {
|
|
136
|
+
return feed.subscribe(callback);
|
|
137
|
+
};
|
|
138
|
+
return callable;
|
|
80
139
|
}
|
|
81
|
-
|
|
82
|
-
|
|
140
|
+
//#endregion
|
|
141
|
+
//#region src/reactive-map.ts
|
|
142
|
+
/**
|
|
143
|
+
* Create a `ReactiveMap<K, V, C>` and its producer-side handle.
|
|
144
|
+
*
|
|
145
|
+
* The reactive map owns its internal `Map<K, V>`. Consumers read via
|
|
146
|
+
* the `ReactiveMap` surface (call signature, `.get()`, `.has()`, etc.).
|
|
147
|
+
* Producers mutate via the `ReactiveMapHandle` (`set`, `delete`,
|
|
148
|
+
* `clear`) and push notifications via `emit`.
|
|
149
|
+
*
|
|
150
|
+
* ```ts
|
|
151
|
+
* const [peers, handle] = createReactiveMap<PeerId, PeerInfo, PeerChange>()
|
|
152
|
+
*
|
|
153
|
+
* handle.set("alice", aliceInfo)
|
|
154
|
+
* handle.emit({ changes: [{ type: "peer-joined", peer: aliceInfo }] })
|
|
155
|
+
*
|
|
156
|
+
* peers() // ReadonlyMap with one entry
|
|
157
|
+
* peers.get("alice") // aliceInfo
|
|
158
|
+
* peers.size // 1
|
|
159
|
+
* ```
|
|
160
|
+
*/
|
|
83
161
|
function createReactiveMap() {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
return [callable, handle];
|
|
162
|
+
const map = /* @__PURE__ */ new Map();
|
|
163
|
+
const [feed, emit] = createChangefeed(() => map);
|
|
164
|
+
const callable = () => map;
|
|
165
|
+
Object.defineProperty(callable, CHANGEFEED, {
|
|
166
|
+
get() {
|
|
167
|
+
return feed[CHANGEFEED];
|
|
168
|
+
},
|
|
169
|
+
enumerable: false,
|
|
170
|
+
configurable: false
|
|
171
|
+
});
|
|
172
|
+
Object.defineProperty(callable, "current", {
|
|
173
|
+
get() {
|
|
174
|
+
return map;
|
|
175
|
+
},
|
|
176
|
+
enumerable: true,
|
|
177
|
+
configurable: false
|
|
178
|
+
});
|
|
179
|
+
callable.subscribe = (callback) => {
|
|
180
|
+
return feed.subscribe(callback);
|
|
181
|
+
};
|
|
182
|
+
callable.get = (key) => map.get(key);
|
|
183
|
+
callable.has = (key) => map.has(key);
|
|
184
|
+
callable.keys = () => map.keys();
|
|
185
|
+
Object.defineProperty(callable, "size", {
|
|
186
|
+
get() {
|
|
187
|
+
return map.size;
|
|
188
|
+
},
|
|
189
|
+
enumerable: true,
|
|
190
|
+
configurable: false
|
|
191
|
+
});
|
|
192
|
+
callable[Symbol.iterator] = () => map[Symbol.iterator]();
|
|
193
|
+
return [callable, {
|
|
194
|
+
set(key, value) {
|
|
195
|
+
map.set(key, value);
|
|
196
|
+
},
|
|
197
|
+
delete(key) {
|
|
198
|
+
return map.delete(key);
|
|
199
|
+
},
|
|
200
|
+
clear() {
|
|
201
|
+
map.clear();
|
|
202
|
+
},
|
|
203
|
+
emit
|
|
204
|
+
}];
|
|
128
205
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
createCallable,
|
|
133
|
-
createChangefeed,
|
|
134
|
-
createReactiveMap,
|
|
135
|
-
hasChangefeed,
|
|
136
|
-
staticChangefeed
|
|
137
|
-
};
|
|
206
|
+
//#endregion
|
|
207
|
+
export { CHANGEFEED, changefeed, createCallable, createChangefeed, createReactiveMap, hasChangefeed, staticChangefeed };
|
|
208
|
+
|
|
138
209
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/changefeed.ts","../src/callable.ts","../src/reactive-map.ts"],"sourcesContent":["// Changefeed — the universal reactive contract.\n//\n// A changefeed is a reactive value with a current state and a stream\n// of future changes. You read `current` to see what's there now;\n// you subscribe to learn what changes next.\n//\n// The changefeed protocol is expressed through a single symbol: CHANGEFEED.\n//\n// Changes are delivered as `Changeset<C>` — a batch of one or more\n// changes with optional provenance metadata. Auto-commit wraps a\n// single change in a degenerate changeset of one; transactions and\n// `applyChanges` deliver multi-change batches. The subscriber API\n// is uniform regardless of batch size.\n//\n// This module is the canonical home of the reactive contract. It has\n// zero dependencies — no schema, no paths, no interpreters.\n\nimport type { ChangeBase } from \"./change.js\"\n\n// ---------------------------------------------------------------------------\n// Symbol\n// ---------------------------------------------------------------------------\n\n/**\n * The single symbol that marks a value as a changefeed. Accessing\n * `obj[CHANGEFEED]` yields a `ChangefeedProtocol<S, C>` — the current\n * value and a stream of future changes.\n *\n * Uses `Symbol.for` so that multiple copies of this module (e.g. in\n * different bundle chunks) share the same symbol identity.\n */\nexport const CHANGEFEED: unique symbol = Symbol.for(\"kyneta:changefeed\") as any\n\n// ---------------------------------------------------------------------------\n// Changeset — the unit of batch delivery\n// ---------------------------------------------------------------------------\n\n/**\n * A changeset is the unit of delivery through the changefeed protocol.\n * It wraps one or more changes with optional batch-level metadata.\n *\n * - Auto-commit produces a degenerate changeset of one change.\n * - Transactions and `applyChanges` produce multi-change batches.\n * - `origin` carries provenance for the entire batch (e.g. \"sync\",\n * \"undo\", \"local\"). Individual changes do not carry provenance —\n * the batch does.\n *\n * The subscriber API always receives a `Changeset`, making it uniform\n * regardless of how the changes were produced.\n */\nexport interface Changeset<C = ChangeBase> {\n /** The individual changes in this batch. */\n readonly changes: readonly C[]\n /** Provenance of the batch (e.g. \"sync\", \"undo\", \"local\"). */\n readonly origin?: string\n}\n\n// ---------------------------------------------------------------------------\n// Core interfaces — protocol layer\n// ---------------------------------------------------------------------------\n\n/**\n * The protocol object that sits behind the `[CHANGEFEED]` symbol.\n *\n * A coalgebra: `current` gives the live state, `subscribe` gives the\n * stream of future changes. In automata-theory terms this is a Moore\n * machine with a push-based transition stream.\n *\n * Properties:\n * - `current` is a getter — always returns the live current value\n * - `subscribe` returns an unsubscribe function\n * - Subscribers receive a `Changeset<C>` — a batch of changes with\n * optional provenance. For auto-commit (single mutation), the\n * changeset contains exactly one change.\n * - Static (non-reactive) sources return a protocol whose tail never emits:\n * `{ current: value, subscribe: () => () => {} }`\n *\n * This is internal plumbing — developers interact with `Changefeed<S, C>`\n * (the developer-facing type that includes `[CHANGEFEED]`, `.current`,\n * and `.subscribe()` in one interface).\n */\nexport interface ChangefeedProtocol<S, C extends ChangeBase = ChangeBase> {\n /** The current value, always live (a getter). */\n readonly current: S\n /** Subscribe to future changes. Returns an unsubscribe function. */\n subscribe(callback: (changeset: Changeset<C>) => void): () => void\n}\n\n// ---------------------------------------------------------------------------\n// Core interfaces — developer-facing type\n// ---------------------------------------------------------------------------\n\n/**\n * The developer-facing changefeed type: a reactive value with direct\n * access to `.current`, `.subscribe()`, and the `[CHANGEFEED]` marker.\n *\n * Developers write `readonly peers: Changefeed<PeerMap, PeerChange>` —\n * no `Has` prefix, no separate protocol object, no triple declaration.\n *\n * A `Changefeed<S, C>` is the intersection of:\n * - The `[CHANGEFEED]` marker (for compiler detection and runtime protocol)\n * - Direct `.current` and `.subscribe()` access (for developer ergonomics)\n *\n * Use `changefeed(source)` to project any `HasChangefeed` into a\n * `Changefeed`, or `createChangefeed()` to build one from scratch.\n */\nexport interface Changefeed<S, C extends ChangeBase = ChangeBase> {\n /** The protocol object behind the symbol. */\n readonly [CHANGEFEED]: ChangefeedProtocol<S, C>\n /** The current value, always live (a getter). */\n readonly current: S\n /** Subscribe to future changes. Returns an unsubscribe function. */\n subscribe(callback: (changeset: Changeset<C>) => void): () => void\n}\n\n/**\n * An object that carries a changefeed protocol under the `[CHANGEFEED]`\n * symbol.\n *\n * Any ref, interpreted node, or enriched value can implement this\n * interface to participate in the reactive protocol.\n */\nexport interface HasChangefeed<S = unknown, A extends ChangeBase = ChangeBase> {\n readonly [CHANGEFEED]: ChangefeedProtocol<S, A>\n}\n\n// ---------------------------------------------------------------------------\n// Type guard\n// ---------------------------------------------------------------------------\n\n/**\n * Returns `true` if `value` has a `[CHANGEFEED]` property, i.e. it\n * implements the `HasChangefeed` interface.\n */\nexport function hasChangefeed<S = unknown, A extends ChangeBase = ChangeBase>(\n value: unknown,\n): value is HasChangefeed<S, A> {\n return (\n value !== null &&\n value !== undefined &&\n (typeof value === \"object\" || typeof value === \"function\") &&\n CHANGEFEED in (value as object)\n )\n}\n\n// ---------------------------------------------------------------------------\n// Static feed helper\n// ---------------------------------------------------------------------------\n\n/**\n * Creates a changefeed protocol that never emits changes — useful for\n * static/non-reactive data sources that still need to participate in\n * the changefeed protocol.\n */\nexport function staticChangefeed<S>(head: S): ChangefeedProtocol<S, never> {\n return {\n get current() {\n return head\n },\n subscribe() {\n return () => {}\n },\n }\n}\n\n// ---------------------------------------------------------------------------\n// Projector — lift HasChangefeed to Changefeed\n// ---------------------------------------------------------------------------\n\n/**\n * Project any object with `[CHANGEFEED]` into a developer-facing\n * `Changefeed<S, C>` — lifting the hidden protocol surface to direct\n * `.current` and `.subscribe()` accessibility.\n *\n * ```ts\n * const feed = changefeed(doc.title)\n * feed.current // live value\n * feed.subscribe(cb) // subscribe to changes\n * feed[CHANGEFEED] // the protocol object (same as doc.title[CHANGEFEED])\n * ```\n */\nexport function changefeed<S, C extends ChangeBase>(\n source: HasChangefeed<S, C>,\n): Changefeed<S, C> {\n const protocol = source[CHANGEFEED]\n return {\n [CHANGEFEED]: protocol,\n get current(): S {\n return protocol.current\n },\n subscribe(callback: (changeset: Changeset<C>) => void): () => void {\n return protocol.subscribe(callback)\n },\n }\n}\n\n// ---------------------------------------------------------------------------\n// Factory — create standalone Changefeed values\n// ---------------------------------------------------------------------------\n\n/**\n * Create a standalone `Changefeed<S, C>` with push semantics.\n *\n * Returns a tuple of the feed and an emit function. The feed's\n * `[CHANGEFEED]` returns the protocol view of itself. Manages its\n * own subscriber set internally.\n *\n * ```ts\n * const [feed, emit] = createChangefeed(() => count)\n * feed.current // read live value\n * feed.subscribe(cs => { ... }) // subscribe\n * hasChangefeed(feed) // true\n * emit({ changes: [{ type: \"replace\", value: 42 }] }) // push\n * ```\n */\nexport function createChangefeed<S, C extends ChangeBase = ChangeBase>(\n getCurrent: () => S,\n): [feed: Changefeed<S, C>, emit: (changeset: Changeset<C>) => void] {\n const subscribers = new Set<(changeset: Changeset<C>) => void>()\n\n const protocol: ChangefeedProtocol<S, C> = {\n get current(): S {\n return getCurrent()\n },\n subscribe(callback: (changeset: Changeset<C>) => void): () => void {\n subscribers.add(callback)\n return () => {\n subscribers.delete(callback)\n }\n },\n }\n\n const feed: Changefeed<S, C> = {\n [CHANGEFEED]: protocol,\n get current(): S {\n return getCurrent()\n },\n subscribe(callback: (changeset: Changeset<C>) => void): () => void {\n return protocol.subscribe(callback)\n },\n }\n\n const emit = (changeset: Changeset<C>): void => {\n for (const cb of subscribers) {\n cb(changeset)\n }\n }\n\n return [feed, emit]\n}\n","// callable — the createCallable combinator.\n//\n// Wraps a Changefeed<S, C> in a callable function-object so that\n// `feed()` returns `feed.current`. The callable preserves the full\n// changefeed contract: [CHANGEFEED], .current, .subscribe().\n//\n// This is the same function-object pattern used by LocalRef in\n// @kyneta/cast — a function with properties attached.\n\nimport type { ChangeBase } from \"./change.js\"\nimport type { Changefeed, ChangefeedProtocol, Changeset } from \"./changefeed.js\"\nimport { CHANGEFEED } from \"./changefeed.js\"\n\n// ---------------------------------------------------------------------------\n// Type\n// ---------------------------------------------------------------------------\n\n/**\n * A changefeed that is also callable — `feed()` returns `feed.current`.\n *\n * This is the intersection of `Changefeed<S, C>` and `() => S`.\n * The call signature provides ergonomic read access without `.current`.\n */\nexport type CallableChangefeed<\n S,\n C extends ChangeBase = ChangeBase,\n> = Changefeed<S, C> & (() => S)\n\n// ---------------------------------------------------------------------------\n// Factory\n// ---------------------------------------------------------------------------\n\n/**\n * Wrap a `Changefeed<S, C>` in a callable function-object.\n *\n * The returned object:\n * - `feed()` → `feed.current` (callable)\n * - `feed.current` → delegated getter\n * - `feed.subscribe(cb)` → delegated\n * - `feed[CHANGEFEED]` → delegated protocol\n * - `hasChangefeed(feed)` → `true`\n *\n * ```ts\n * const [source, emit] = createChangefeed(() => count)\n * const feed = createCallable(source)\n * feed() // read current value\n * feed.current // same as feed()\n * feed.subscribe(cb) // subscribe to changes\n * ```\n */\nexport function createCallable<S, C extends ChangeBase>(\n feed: Changefeed<S, C>,\n): CallableChangefeed<S, C> {\n const callable: any = () => feed.current\n\n // [CHANGEFEED] — non-enumerable getter delegating to source\n Object.defineProperty(callable, CHANGEFEED, {\n get(): ChangefeedProtocol<S, C> {\n return feed[CHANGEFEED]\n },\n enumerable: false,\n configurable: false,\n })\n\n // .current — getter delegating to source\n Object.defineProperty(callable, \"current\", {\n get(): S {\n return feed.current\n },\n enumerable: true,\n configurable: false,\n })\n\n // .subscribe — delegating to source\n callable.subscribe = (\n callback: (changeset: Changeset<C>) => void,\n ): (() => void) => {\n return feed.subscribe(callback)\n }\n\n return callable as CallableChangefeed<S, C>\n}\n","// reactive-map — a callable changefeed over a mutable Map.\n//\n// ReactiveMap<K, V, C> is a CallableChangefeed<ReadonlyMap<K, V>, C>\n// with lifted collection accessors (.get, .has, .keys, .size, iteration).\n// The handle provides raw map mutations (set, delete, clear) without\n// automatic emission — the consumer decides when and what to emit.\n//\n// This extracts the recurring pattern of \"callable changefeed over a\n// ReadonlyMap with convenience accessors\" (used by exchange.peers,\n// Catalog, and future reactive collections) into a single combinator.\n\nimport type { CallableChangefeed } from \"./callable.js\"\nimport type { ChangeBase } from \"./change.js\"\nimport type { ChangefeedProtocol, Changeset } from \"./changefeed.js\"\nimport { CHANGEFEED, createChangefeed } from \"./changefeed.js\"\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/**\n * A callable changefeed over a `ReadonlyMap<K, V>` with lifted\n * collection accessors.\n *\n * `reactiveMap()` returns the current `ReadonlyMap<K, V>`.\n * `.get()`, `.has()`, `.keys()`, `.size`, and `[Symbol.iterator]()`\n * delegate to the internal map — no need to unwrap `.current` first.\n *\n * Extends `CallableChangefeed` — assignable anywhere a\n * `CallableChangefeed<ReadonlyMap<K, V>, C>` or `Changefeed` is expected.\n */\nexport interface ReactiveMap<K, V, C extends ChangeBase = ChangeBase>\n extends CallableChangefeed<ReadonlyMap<K, V>, C> {\n /** Get the value for a key, or `undefined` if absent. */\n get(key: K): V | undefined\n /** Whether the map contains a key. */\n has(key: K): boolean\n /** An iterator over all keys. */\n keys(): IterableIterator<K>\n /** The number of entries. */\n readonly size: number\n /** Iterate over `[key, value]` pairs. */\n [Symbol.iterator](): IterableIterator<[K, V]>\n}\n\n/**\n * The producer-side handle for a `ReactiveMap`.\n *\n * Provides raw map mutations (`set`, `delete`, `clear`) that modify\n * the internal map **without** emitting changes. Call `emit()` with\n * the appropriate changeset after mutations are complete.\n *\n * This separation lets the consumer batch mutations and emit a single\n * changeset — e.g. `clear()` → N × `set()` → one `emit()`.\n */\nexport interface ReactiveMapHandle<K, V, C extends ChangeBase> {\n /** Insert or overwrite an entry. Does NOT emit. */\n set(key: K, value: V): void\n /** Remove an entry. Returns `true` if the key was present. Does NOT emit. */\n delete(key: K): boolean\n /** Remove all entries. Does NOT emit. */\n clear(): void\n /** Push a changeset to all subscribers. */\n emit(changeset: Changeset<C>): void\n}\n\n// ---------------------------------------------------------------------------\n// Factory\n// ---------------------------------------------------------------------------\n\n/**\n * Create a `ReactiveMap<K, V, C>` and its producer-side handle.\n *\n * The reactive map owns its internal `Map<K, V>`. Consumers read via\n * the `ReactiveMap` surface (call signature, `.get()`, `.has()`, etc.).\n * Producers mutate via the `ReactiveMapHandle` (`set`, `delete`,\n * `clear`) and push notifications via `emit`.\n *\n * ```ts\n * const [peers, handle] = createReactiveMap<PeerId, PeerInfo, PeerChange>()\n *\n * handle.set(\"alice\", aliceInfo)\n * handle.emit({ changes: [{ type: \"peer-joined\", peer: aliceInfo }] })\n *\n * peers() // ReadonlyMap with one entry\n * peers.get(\"alice\") // aliceInfo\n * peers.size // 1\n * ```\n */\nexport function createReactiveMap<K, V, C extends ChangeBase = ChangeBase>(): [\n ReactiveMap<K, V, C>,\n ReactiveMapHandle<K, V, C>,\n] {\n const map = new Map<K, V>()\n\n // Create the base changefeed + emit pair.\n // The thunk reads the same Map instance — never reassigned.\n const [feed, emit] = createChangefeed<ReadonlyMap<K, V>, C>(() => map)\n\n // Build the callable function-object.\n // We construct it manually (rather than using createCallable) so we\n // can attach the collection accessors in one pass.\n const callable: any = () => map as ReadonlyMap<K, V>\n\n // ── Changefeed protocol ──\n\n Object.defineProperty(callable, CHANGEFEED, {\n get(): ChangefeedProtocol<ReadonlyMap<K, V>, C> {\n return feed[CHANGEFEED]\n },\n enumerable: false,\n configurable: false,\n })\n\n Object.defineProperty(callable, \"current\", {\n get(): ReadonlyMap<K, V> {\n return map\n },\n enumerable: true,\n configurable: false,\n })\n\n callable.subscribe = (\n callback: (changeset: Changeset<C>) => void,\n ): (() => void) => {\n return feed.subscribe(callback)\n }\n\n // ── Lifted collection accessors ──\n\n callable.get = (key: K): V | undefined => map.get(key)\n callable.has = (key: K): boolean => map.has(key)\n callable.keys = (): IterableIterator<K> => map.keys()\n\n Object.defineProperty(callable, \"size\", {\n get(): number {\n return map.size\n },\n enumerable: true,\n configurable: false,\n })\n\n callable[Symbol.iterator] = (): IterableIterator<[K, V]> =>\n map[Symbol.iterator]()\n\n // ── Handle (producer side) ──\n\n const handle: ReactiveMapHandle<K, V, C> = {\n set(key: K, value: V): void {\n map.set(key, value)\n },\n delete(key: K): boolean {\n return map.delete(key)\n },\n clear(): void {\n map.clear()\n },\n emit,\n }\n\n return [callable as ReactiveMap<K, V, C>, handle]\n}\n"],"mappings":";AA+BO,IAAM,aAA4B,uBAAO,IAAI,mBAAmB;AAuGhE,SAAS,cACd,OAC8B;AAC9B,SACE,UAAU,QACV,UAAU,WACT,OAAO,UAAU,YAAY,OAAO,UAAU,eAC/C,cAAe;AAEnB;AAWO,SAAS,iBAAoB,MAAuC;AACzE,SAAO;AAAA,IACL,IAAI,UAAU;AACZ,aAAO;AAAA,IACT;AAAA,IACA,YAAY;AACV,aAAO,MAAM;AAAA,MAAC;AAAA,IAChB;AAAA,EACF;AACF;AAkBO,SAAS,WACd,QACkB;AAClB,QAAM,WAAW,OAAO,UAAU;AAClC,SAAO;AAAA,IACL,CAAC,UAAU,GAAG;AAAA,IACd,IAAI,UAAa;AACf,aAAO,SAAS;AAAA,IAClB;AAAA,IACA,UAAU,UAAyD;AACjE,aAAO,SAAS,UAAU,QAAQ;AAAA,IACpC;AAAA,EACF;AACF;AAqBO,SAAS,iBACd,YACmE;AACnE,QAAM,cAAc,oBAAI,IAAuC;AAE/D,QAAM,WAAqC;AAAA,IACzC,IAAI,UAAa;AACf,aAAO,WAAW;AAAA,IACpB;AAAA,IACA,UAAU,UAAyD;AACjE,kBAAY,IAAI,QAAQ;AACxB,aAAO,MAAM;AACX,oBAAY,OAAO,QAAQ;AAAA,MAC7B;AAAA,IACF;AAAA,EACF;AAEA,QAAM,OAAyB;AAAA,IAC7B,CAAC,UAAU,GAAG;AAAA,IACd,IAAI,UAAa;AACf,aAAO,WAAW;AAAA,IACpB;AAAA,IACA,UAAU,UAAyD;AACjE,aAAO,SAAS,UAAU,QAAQ;AAAA,IACpC;AAAA,EACF;AAEA,QAAM,OAAO,CAAC,cAAkC;AAC9C,eAAW,MAAM,aAAa;AAC5B,SAAG,SAAS;AAAA,IACd;AAAA,EACF;AAEA,SAAO,CAAC,MAAM,IAAI;AACpB;;;ACvMO,SAAS,eACd,MAC0B;AAC1B,QAAM,WAAgB,MAAM,KAAK;AAGjC,SAAO,eAAe,UAAU,YAAY;AAAA,IAC1C,MAAgC;AAC9B,aAAO,KAAK,UAAU;AAAA,IACxB;AAAA,IACA,YAAY;AAAA,IACZ,cAAc;AAAA,EAChB,CAAC;AAGD,SAAO,eAAe,UAAU,WAAW;AAAA,IACzC,MAAS;AACP,aAAO,KAAK;AAAA,IACd;AAAA,IACA,YAAY;AAAA,IACZ,cAAc;AAAA,EAChB,CAAC;AAGD,WAAS,YAAY,CACnB,aACiB;AACjB,WAAO,KAAK,UAAU,QAAQ;AAAA,EAChC;AAEA,SAAO;AACT;;;ACQO,SAAS,oBAGd;AACA,QAAM,MAAM,oBAAI,IAAU;AAI1B,QAAM,CAAC,MAAM,IAAI,IAAI,iBAAuC,MAAM,GAAG;AAKrE,QAAM,WAAgB,MAAM;AAI5B,SAAO,eAAe,UAAU,YAAY;AAAA,IAC1C,MAAgD;AAC9C,aAAO,KAAK,UAAU;AAAA,IACxB;AAAA,IACA,YAAY;AAAA,IACZ,cAAc;AAAA,EAChB,CAAC;AAED,SAAO,eAAe,UAAU,WAAW;AAAA,IACzC,MAAyB;AACvB,aAAO;AAAA,IACT;AAAA,IACA,YAAY;AAAA,IACZ,cAAc;AAAA,EAChB,CAAC;AAED,WAAS,YAAY,CACnB,aACiB;AACjB,WAAO,KAAK,UAAU,QAAQ;AAAA,EAChC;AAIA,WAAS,MAAM,CAAC,QAA0B,IAAI,IAAI,GAAG;AACrD,WAAS,MAAM,CAAC,QAAoB,IAAI,IAAI,GAAG;AAC/C,WAAS,OAAO,MAA2B,IAAI,KAAK;AAEpD,SAAO,eAAe,UAAU,QAAQ;AAAA,IACtC,MAAc;AACZ,aAAO,IAAI;AAAA,IACb;AAAA,IACA,YAAY;AAAA,IACZ,cAAc;AAAA,EAChB,CAAC;AAED,WAAS,OAAO,QAAQ,IAAI,MAC1B,IAAI,OAAO,QAAQ,EAAE;AAIvB,QAAM,SAAqC;AAAA,IACzC,IAAI,KAAQ,OAAgB;AAC1B,UAAI,IAAI,KAAK,KAAK;AAAA,IACpB;AAAA,IACA,OAAO,KAAiB;AACtB,aAAO,IAAI,OAAO,GAAG;AAAA,IACvB;AAAA,IACA,QAAc;AACZ,UAAI,MAAM;AAAA,IACZ;AAAA,IACA;AAAA,EACF;AAEA,SAAO,CAAC,UAAkC,MAAM;AAClD;","names":[]}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/changefeed.ts","../src/callable.ts","../src/reactive-map.ts"],"sourcesContent":["// Changefeed — the universal reactive contract.\n//\n// A changefeed is a reactive value with a current state and a stream\n// of future changes. You read `current` to see what's there now;\n// you subscribe to learn what changes next.\n//\n// The changefeed protocol is expressed through a single symbol: CHANGEFEED.\n//\n// Changes are delivered as `Changeset<C>` — a batch of one or more\n// changes with optional provenance metadata. Auto-commit wraps a\n// single change in a degenerate changeset of one; transactions and\n// `applyChanges` deliver multi-change batches. The subscriber API\n// is uniform regardless of batch size.\n//\n// This module is the canonical home of the reactive contract. It has\n// zero dependencies — no schema, no paths, no interpreters.\n\nimport type { ChangeBase } from \"./change.js\"\n\n// ---------------------------------------------------------------------------\n// Symbol\n// ---------------------------------------------------------------------------\n\n/**\n * The single symbol that marks a value as a changefeed. Accessing\n * `obj[CHANGEFEED]` yields a `ChangefeedProtocol<S, C>` — the current\n * value and a stream of future changes.\n *\n * Uses `Symbol.for` so that multiple copies of this module (e.g. in\n * different bundle chunks) share the same symbol identity.\n */\nexport const CHANGEFEED: unique symbol = Symbol.for(\"kyneta:changefeed\") as any\n\n// ---------------------------------------------------------------------------\n// Changeset — the unit of batch delivery\n// ---------------------------------------------------------------------------\n\n/**\n * A changeset is the unit of delivery through the changefeed protocol.\n * It wraps one or more changes with optional batch-level metadata.\n *\n * - Auto-commit produces a degenerate changeset of one change.\n * - Transactions and `applyChanges` produce multi-change batches.\n * - `origin` carries provenance for the entire batch (e.g. \"sync\",\n * \"undo\", \"local\"). Individual changes do not carry provenance —\n * the batch does.\n *\n * The subscriber API always receives a `Changeset`, making it uniform\n * regardless of how the changes were produced.\n */\nexport interface Changeset<C = ChangeBase> {\n /** The individual changes in this batch. */\n readonly changes: readonly C[]\n /** Provenance of the batch (e.g. \"sync\", \"undo\", \"local\"). */\n readonly origin?: string\n}\n\n// ---------------------------------------------------------------------------\n// Core interfaces — protocol layer\n// ---------------------------------------------------------------------------\n\n/**\n * The protocol object that sits behind the `[CHANGEFEED]` symbol.\n *\n * A coalgebra: `current` gives the live state, `subscribe` gives the\n * stream of future changes. In automata-theory terms this is a Moore\n * machine with a push-based transition stream.\n *\n * Properties:\n * - `current` is a getter — always returns the live current value\n * - `subscribe` returns an unsubscribe function\n * - Subscribers receive a `Changeset<C>` — a batch of changes with\n * optional provenance. For auto-commit (single mutation), the\n * changeset contains exactly one change.\n * - Static (non-reactive) sources return a protocol whose tail never emits:\n * `{ current: value, subscribe: () => () => {} }`\n *\n * This is internal plumbing — developers interact with `Changefeed<S, C>`\n * (the developer-facing type that includes `[CHANGEFEED]`, `.current`,\n * and `.subscribe()` in one interface).\n */\nexport interface ChangefeedProtocol<S, C extends ChangeBase = ChangeBase> {\n /** The current value, always live (a getter). */\n readonly current: S\n /** Subscribe to future changes. Returns an unsubscribe function. */\n subscribe(callback: (changeset: Changeset<C>) => void): () => void\n}\n\n// ---------------------------------------------------------------------------\n// Core interfaces — developer-facing type\n// ---------------------------------------------------------------------------\n\n/**\n * The developer-facing changefeed type: a reactive value with direct\n * access to `.current`, `.subscribe()`, and the `[CHANGEFEED]` marker.\n *\n * Developers write `readonly peers: Changefeed<PeerMap, PeerChange>` —\n * no `Has` prefix, no separate protocol object, no triple declaration.\n *\n * A `Changefeed<S, C>` is the intersection of:\n * - The `[CHANGEFEED]` marker (for compiler detection and runtime protocol)\n * - Direct `.current` and `.subscribe()` access (for developer ergonomics)\n *\n * Use `changefeed(source)` to project any `HasChangefeed` into a\n * `Changefeed`, or `createChangefeed()` to build one from scratch.\n */\nexport interface Changefeed<S, C extends ChangeBase = ChangeBase> {\n /** The protocol object behind the symbol. */\n readonly [CHANGEFEED]: ChangefeedProtocol<S, C>\n /** The current value, always live (a getter). */\n readonly current: S\n /** Subscribe to future changes. Returns an unsubscribe function. */\n subscribe(callback: (changeset: Changeset<C>) => void): () => void\n}\n\n/**\n * An object that carries a changefeed protocol under the `[CHANGEFEED]`\n * symbol.\n *\n * Any ref, interpreted node, or enriched value can implement this\n * interface to participate in the reactive protocol.\n */\nexport interface HasChangefeed<S = unknown, A extends ChangeBase = ChangeBase> {\n readonly [CHANGEFEED]: ChangefeedProtocol<S, A>\n}\n\n// ---------------------------------------------------------------------------\n// Type guard\n// ---------------------------------------------------------------------------\n\n/**\n * Returns `true` if `value` has a `[CHANGEFEED]` property, i.e. it\n * implements the `HasChangefeed` interface.\n */\nexport function hasChangefeed<S = unknown, A extends ChangeBase = ChangeBase>(\n value: unknown,\n): value is HasChangefeed<S, A> {\n return (\n value !== null &&\n value !== undefined &&\n (typeof value === \"object\" || typeof value === \"function\") &&\n CHANGEFEED in (value as object)\n )\n}\n\n// ---------------------------------------------------------------------------\n// Static feed helper\n// ---------------------------------------------------------------------------\n\n/**\n * Creates a changefeed protocol that never emits changes — useful for\n * static/non-reactive data sources that still need to participate in\n * the changefeed protocol.\n */\nexport function staticChangefeed<S>(head: S): ChangefeedProtocol<S, never> {\n return {\n get current() {\n return head\n },\n subscribe() {\n return () => {}\n },\n }\n}\n\n// ---------------------------------------------------------------------------\n// Projector — lift HasChangefeed to Changefeed\n// ---------------------------------------------------------------------------\n\n/**\n * Project any object with `[CHANGEFEED]` into a developer-facing\n * `Changefeed<S, C>` — lifting the hidden protocol surface to direct\n * `.current` and `.subscribe()` accessibility.\n *\n * ```ts\n * const feed = changefeed(doc.title)\n * feed.current // live value\n * feed.subscribe(cb) // subscribe to changes\n * feed[CHANGEFEED] // the protocol object (same as doc.title[CHANGEFEED])\n * ```\n */\nexport function changefeed<S, C extends ChangeBase>(\n source: HasChangefeed<S, C>,\n): Changefeed<S, C> {\n const protocol = source[CHANGEFEED]\n return {\n [CHANGEFEED]: protocol,\n get current(): S {\n return protocol.current\n },\n subscribe(callback: (changeset: Changeset<C>) => void): () => void {\n return protocol.subscribe(callback)\n },\n }\n}\n\n// ---------------------------------------------------------------------------\n// Factory — create standalone Changefeed values\n// ---------------------------------------------------------------------------\n\n/**\n * Create a standalone `Changefeed<S, C>` with push semantics.\n *\n * Returns a tuple of the feed and an emit function. The feed's\n * `[CHANGEFEED]` returns the protocol view of itself. Manages its\n * own subscriber set internally.\n *\n * ```ts\n * const [feed, emit] = createChangefeed(() => count)\n * feed.current // read live value\n * feed.subscribe(cs => { ... }) // subscribe\n * hasChangefeed(feed) // true\n * emit({ changes: [{ type: \"replace\", value: 42 }] }) // push\n * ```\n */\nexport function createChangefeed<S, C extends ChangeBase = ChangeBase>(\n getCurrent: () => S,\n): [feed: Changefeed<S, C>, emit: (changeset: Changeset<C>) => void] {\n const subscribers = new Set<(changeset: Changeset<C>) => void>()\n\n const protocol: ChangefeedProtocol<S, C> = {\n get current(): S {\n return getCurrent()\n },\n subscribe(callback: (changeset: Changeset<C>) => void): () => void {\n subscribers.add(callback)\n return () => {\n subscribers.delete(callback)\n }\n },\n }\n\n const feed: Changefeed<S, C> = {\n [CHANGEFEED]: protocol,\n get current(): S {\n return getCurrent()\n },\n subscribe(callback: (changeset: Changeset<C>) => void): () => void {\n return protocol.subscribe(callback)\n },\n }\n\n const emit = (changeset: Changeset<C>): void => {\n for (const cb of subscribers) {\n cb(changeset)\n }\n }\n\n return [feed, emit]\n}\n","// callable — the createCallable combinator.\n//\n// Wraps a Changefeed<S, C> in a callable function-object so that\n// `feed()` returns `feed.current`. The callable preserves the full\n// changefeed contract: [CHANGEFEED], .current, .subscribe().\n//\n// This is the same function-object pattern used by LocalRef in\n// @kyneta/cast — a function with properties attached.\n\nimport type { ChangeBase } from \"./change.js\"\nimport type { Changefeed, ChangefeedProtocol, Changeset } from \"./changefeed.js\"\nimport { CHANGEFEED } from \"./changefeed.js\"\n\n// ---------------------------------------------------------------------------\n// Type\n// ---------------------------------------------------------------------------\n\n/**\n * A changefeed that is also callable — `feed()` returns `feed.current`.\n *\n * This is the intersection of `Changefeed<S, C>` and `() => S`.\n * The call signature provides ergonomic read access without `.current`.\n */\nexport type CallableChangefeed<\n S,\n C extends ChangeBase = ChangeBase,\n> = Changefeed<S, C> & (() => S)\n\n// ---------------------------------------------------------------------------\n// Factory\n// ---------------------------------------------------------------------------\n\n/**\n * Wrap a `Changefeed<S, C>` in a callable function-object.\n *\n * The returned object:\n * - `feed()` → `feed.current` (callable)\n * - `feed.current` → delegated getter\n * - `feed.subscribe(cb)` → delegated\n * - `feed[CHANGEFEED]` → delegated protocol\n * - `hasChangefeed(feed)` → `true`\n *\n * ```ts\n * const [source, emit] = createChangefeed(() => count)\n * const feed = createCallable(source)\n * feed() // read current value\n * feed.current // same as feed()\n * feed.subscribe(cb) // subscribe to changes\n * ```\n */\nexport function createCallable<S, C extends ChangeBase>(\n feed: Changefeed<S, C>,\n): CallableChangefeed<S, C> {\n const callable: any = () => feed.current\n\n // [CHANGEFEED] — non-enumerable getter delegating to source\n Object.defineProperty(callable, CHANGEFEED, {\n get(): ChangefeedProtocol<S, C> {\n return feed[CHANGEFEED]\n },\n enumerable: false,\n configurable: false,\n })\n\n // .current — getter delegating to source\n Object.defineProperty(callable, \"current\", {\n get(): S {\n return feed.current\n },\n enumerable: true,\n configurable: false,\n })\n\n // .subscribe — delegating to source\n callable.subscribe = (\n callback: (changeset: Changeset<C>) => void,\n ): (() => void) => {\n return feed.subscribe(callback)\n }\n\n return callable as CallableChangefeed<S, C>\n}\n","// reactive-map — a callable changefeed over a mutable Map.\n//\n// ReactiveMap<K, V, C> is a CallableChangefeed<ReadonlyMap<K, V>, C>\n// with lifted collection accessors (.get, .has, .keys, .size, iteration).\n// The handle provides raw map mutations (set, delete, clear) without\n// automatic emission — the consumer decides when and what to emit.\n//\n// This extracts the recurring pattern of \"callable changefeed over a\n// ReadonlyMap with convenience accessors\" (used by exchange.peers,\n// Catalog, and future reactive collections) into a single combinator.\n\nimport type { CallableChangefeed } from \"./callable.js\"\nimport type { ChangeBase } from \"./change.js\"\nimport type { ChangefeedProtocol, Changeset } from \"./changefeed.js\"\nimport { CHANGEFEED, createChangefeed } from \"./changefeed.js\"\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/**\n * A callable changefeed over a `ReadonlyMap<K, V>` with lifted\n * collection accessors.\n *\n * `reactiveMap()` returns the current `ReadonlyMap<K, V>`.\n * `.get()`, `.has()`, `.keys()`, `.size`, and `[Symbol.iterator]()`\n * delegate to the internal map — no need to unwrap `.current` first.\n *\n * Extends `CallableChangefeed` — assignable anywhere a\n * `CallableChangefeed<ReadonlyMap<K, V>, C>` or `Changefeed` is expected.\n */\nexport interface ReactiveMap<K, V, C extends ChangeBase = ChangeBase>\n extends CallableChangefeed<ReadonlyMap<K, V>, C> {\n /** Get the value for a key, or `undefined` if absent. */\n get(key: K): V | undefined\n /** Whether the map contains a key. */\n has(key: K): boolean\n /** An iterator over all keys. */\n keys(): IterableIterator<K>\n /** The number of entries. */\n readonly size: number\n /** Iterate over `[key, value]` pairs. */\n [Symbol.iterator](): IterableIterator<[K, V]>\n}\n\n/**\n * The producer-side handle for a `ReactiveMap`.\n *\n * Provides raw map mutations (`set`, `delete`, `clear`) that modify\n * the internal map **without** emitting changes. Call `emit()` with\n * the appropriate changeset after mutations are complete.\n *\n * This separation lets the consumer batch mutations and emit a single\n * changeset — e.g. `clear()` → N × `set()` → one `emit()`.\n */\nexport interface ReactiveMapHandle<K, V, C extends ChangeBase> {\n /** Insert or overwrite an entry. Does NOT emit. */\n set(key: K, value: V): void\n /** Remove an entry. Returns `true` if the key was present. Does NOT emit. */\n delete(key: K): boolean\n /** Remove all entries. Does NOT emit. */\n clear(): void\n /** Push a changeset to all subscribers. */\n emit(changeset: Changeset<C>): void\n}\n\n// ---------------------------------------------------------------------------\n// Factory\n// ---------------------------------------------------------------------------\n\n/**\n * Create a `ReactiveMap<K, V, C>` and its producer-side handle.\n *\n * The reactive map owns its internal `Map<K, V>`. Consumers read via\n * the `ReactiveMap` surface (call signature, `.get()`, `.has()`, etc.).\n * Producers mutate via the `ReactiveMapHandle` (`set`, `delete`,\n * `clear`) and push notifications via `emit`.\n *\n * ```ts\n * const [peers, handle] = createReactiveMap<PeerId, PeerInfo, PeerChange>()\n *\n * handle.set(\"alice\", aliceInfo)\n * handle.emit({ changes: [{ type: \"peer-joined\", peer: aliceInfo }] })\n *\n * peers() // ReadonlyMap with one entry\n * peers.get(\"alice\") // aliceInfo\n * peers.size // 1\n * ```\n */\nexport function createReactiveMap<K, V, C extends ChangeBase = ChangeBase>(): [\n ReactiveMap<K, V, C>,\n ReactiveMapHandle<K, V, C>,\n] {\n const map = new Map<K, V>()\n\n // Create the base changefeed + emit pair.\n // The thunk reads the same Map instance — never reassigned.\n const [feed, emit] = createChangefeed<ReadonlyMap<K, V>, C>(() => map)\n\n // Build the callable function-object.\n // We construct it manually (rather than using createCallable) so we\n // can attach the collection accessors in one pass.\n const callable: any = () => map as ReadonlyMap<K, V>\n\n // ── Changefeed protocol ──\n\n Object.defineProperty(callable, CHANGEFEED, {\n get(): ChangefeedProtocol<ReadonlyMap<K, V>, C> {\n return feed[CHANGEFEED]\n },\n enumerable: false,\n configurable: false,\n })\n\n Object.defineProperty(callable, \"current\", {\n get(): ReadonlyMap<K, V> {\n return map\n },\n enumerable: true,\n configurable: false,\n })\n\n callable.subscribe = (\n callback: (changeset: Changeset<C>) => void,\n ): (() => void) => {\n return feed.subscribe(callback)\n }\n\n // ── Lifted collection accessors ──\n\n callable.get = (key: K): V | undefined => map.get(key)\n callable.has = (key: K): boolean => map.has(key)\n callable.keys = (): IterableIterator<K> => map.keys()\n\n Object.defineProperty(callable, \"size\", {\n get(): number {\n return map.size\n },\n enumerable: true,\n configurable: false,\n })\n\n callable[Symbol.iterator] = (): IterableIterator<[K, V]> =>\n map[Symbol.iterator]()\n\n // ── Handle (producer side) ──\n\n const handle: ReactiveMapHandle<K, V, C> = {\n set(key: K, value: V): void {\n map.set(key, value)\n },\n delete(key: K): boolean {\n return map.delete(key)\n },\n clear(): void {\n map.clear()\n },\n emit,\n }\n\n return [callable as ReactiveMap<K, V, C>, handle]\n}\n"],"mappings":";;;;;;;;;AA+BA,MAAa,aAA4B,OAAO,IAAI,oBAAoB;;;;;AAuGxE,SAAgB,cACd,OAC8B;AAC9B,QACE,UAAU,QACV,UAAU,KAAA,MACT,OAAO,UAAU,YAAY,OAAO,UAAU,eAC/C,cAAe;;;;;;;AAanB,SAAgB,iBAAoB,MAAuC;AACzE,QAAO;EACL,IAAI,UAAU;AACZ,UAAO;;EAET,YAAY;AACV,gBAAa;;EAEhB;;;;;;;;;;;;;;AAmBH,SAAgB,WACd,QACkB;CAClB,MAAM,WAAW,OAAO;AACxB,QAAO;GACJ,aAAa;EACd,IAAI,UAAa;AACf,UAAO,SAAS;;EAElB,UAAU,UAAyD;AACjE,UAAO,SAAS,UAAU,SAAS;;EAEtC;;;;;;;;;;;;;;;;;AAsBH,SAAgB,iBACd,YACmE;CACnE,MAAM,8BAAc,IAAI,KAAwC;CAEhE,MAAM,WAAqC;EACzC,IAAI,UAAa;AACf,UAAO,YAAY;;EAErB,UAAU,UAAyD;AACjE,eAAY,IAAI,SAAS;AACzB,gBAAa;AACX,gBAAY,OAAO,SAAS;;;EAGjC;CAED,MAAM,OAAyB;GAC5B,aAAa;EACd,IAAI,UAAa;AACf,UAAO,YAAY;;EAErB,UAAU,UAAyD;AACjE,UAAO,SAAS,UAAU,SAAS;;EAEtC;CAED,MAAM,QAAQ,cAAkC;AAC9C,OAAK,MAAM,MAAM,YACf,IAAG,UAAU;;AAIjB,QAAO,CAAC,MAAM,KAAK;;;;;;;;;;;;;;;;;;;;;;ACtMrB,SAAgB,eACd,MAC0B;CAC1B,MAAM,iBAAsB,KAAK;AAGjC,QAAO,eAAe,UAAU,YAAY;EAC1C,MAAgC;AAC9B,UAAO,KAAK;;EAEd,YAAY;EACZ,cAAc;EACf,CAAC;AAGF,QAAO,eAAe,UAAU,WAAW;EACzC,MAAS;AACP,UAAO,KAAK;;EAEd,YAAY;EACZ,cAAc;EACf,CAAC;AAGF,UAAS,aACP,aACiB;AACjB,SAAO,KAAK,UAAU,SAAS;;AAGjC,QAAO;;;;;;;;;;;;;;;;;;;;;;;ACST,SAAgB,oBAGd;CACA,MAAM,sBAAM,IAAI,KAAW;CAI3B,MAAM,CAAC,MAAM,QAAQ,uBAA6C,IAAI;CAKtE,MAAM,iBAAsB;AAI5B,QAAO,eAAe,UAAU,YAAY;EAC1C,MAAgD;AAC9C,UAAO,KAAK;;EAEd,YAAY;EACZ,cAAc;EACf,CAAC;AAEF,QAAO,eAAe,UAAU,WAAW;EACzC,MAAyB;AACvB,UAAO;;EAET,YAAY;EACZ,cAAc;EACf,CAAC;AAEF,UAAS,aACP,aACiB;AACjB,SAAO,KAAK,UAAU,SAAS;;AAKjC,UAAS,OAAO,QAA0B,IAAI,IAAI,IAAI;AACtD,UAAS,OAAO,QAAoB,IAAI,IAAI,IAAI;AAChD,UAAS,aAAkC,IAAI,MAAM;AAErD,QAAO,eAAe,UAAU,QAAQ;EACtC,MAAc;AACZ,UAAO,IAAI;;EAEb,YAAY;EACZ,cAAc;EACf,CAAC;AAEF,UAAS,OAAO,kBACd,IAAI,OAAO,WAAW;AAiBxB,QAAO,CAAC,UAbmC;EACzC,IAAI,KAAQ,OAAgB;AAC1B,OAAI,IAAI,KAAK,MAAM;;EAErB,OAAO,KAAiB;AACtB,UAAO,IAAI,OAAO,IAAI;;EAExB,QAAc;AACZ,OAAI,OAAO;;EAEb;EACD,CAEgD"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kyneta/changefeed",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "Universal reactive contract — a Moore machine identified by [CHANGEFEED]",
|
|
5
5
|
"author": "Duane Johnson",
|
|
6
6
|
"license": "MIT",
|
|
@@ -30,12 +30,12 @@
|
|
|
30
30
|
"./src/*": "./src/*"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
|
-
"
|
|
33
|
+
"tsdown": "^0.21.9",
|
|
34
34
|
"typescript": "^5.9.2",
|
|
35
35
|
"vitest": "^4.0.17"
|
|
36
36
|
},
|
|
37
37
|
"scripts": {
|
|
38
|
-
"build": "
|
|
38
|
+
"build": "tsdown",
|
|
39
39
|
"test": "verify logic",
|
|
40
40
|
"verify": "verify"
|
|
41
41
|
}
|