@chelone/core 0.5.0 → 0.7.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/assistant/assistant-session.d.ts +28 -0
- package/dist/assistant/assistant-session.js +208 -0
- package/dist/assistant/echo-transport.d.ts +7 -0
- package/dist/assistant/echo-transport.js +28 -0
- package/dist/assistant/http-transport.d.ts +10 -0
- package/dist/assistant/http-transport.js +98 -0
- package/dist/assistant/types.d.ts +54 -0
- package/dist/auth/access-control.d.ts +13 -5
- package/dist/auth/access-control.js +25 -21
- package/dist/auth/auth-options.d.ts +21 -17
- package/dist/auth/auth-options.js +80 -48
- package/dist/auth/permission-matrix.d.ts +12 -0
- package/dist/auth/permission-matrix.js +67 -0
- package/dist/auth/types.d.ts +14 -4
- package/dist/block-editor/block-document.d.ts +60 -0
- package/dist/block-editor/block-document.js +143 -0
- package/dist/block-editor/data-blocks.d.ts +63 -0
- package/dist/block-editor/data-blocks.js +117 -0
- package/dist/chart/chart-data.d.ts +25 -0
- package/dist/chart/chart-data.js +61 -0
- package/dist/command-menu/hotkey.js +58 -47
- package/dist/command-menu/recents.js +8 -11
- package/dist/command-menu/registry.d.ts +9 -5
- package/dist/command-menu/registry.js +36 -46
- package/dist/controller/calendar-dates.js +82 -113
- package/dist/controller/calendar-events.js +93 -88
- package/dist/controller/client-list-controller.d.ts +2 -2
- package/dist/controller/client-list-controller.js +251 -254
- package/dist/controller/data-table-columns.d.ts +2 -0
- package/dist/controller/data-table-columns.js +50 -56
- package/dist/controller/deep-equal.d.ts +1 -0
- package/dist/controller/deep-equal.js +14 -0
- package/dist/controller/filter-toggle.js +36 -44
- package/dist/controller/in-place-editor.d.ts +42 -0
- package/dist/controller/in-place-editor.js +42 -0
- package/dist/controller/infinite-list-controller.js +220 -190
- package/dist/controller/inspector.d.ts +49 -0
- package/dist/controller/inspector.js +64 -0
- package/dist/controller/job-monitor.d.ts +41 -0
- package/dist/controller/job-monitor.js +49 -0
- package/dist/controller/kanban-board.d.ts +2 -3
- package/dist/controller/kanban-board.js +43 -24
- package/dist/controller/list-controller.d.ts +5 -5
- package/dist/controller/list-controller.js +444 -400
- package/dist/controller/list-params.js +96 -87
- package/dist/controller/pivot-grid.d.ts +36 -0
- package/dist/controller/pivot-grid.js +99 -0
- package/dist/controller/prev-next.d.ts +2 -2
- package/dist/controller/prev-next.js +74 -54
- package/dist/controller/record-controllers.d.ts +14 -6
- package/dist/controller/record-controllers.js +288 -257
- package/dist/controller/record-diff.d.ts +21 -0
- package/dist/controller/record-diff.js +50 -0
- package/dist/controller/record-timeline.d.ts +35 -0
- package/dist/controller/record-timeline.js +48 -0
- package/dist/controller/record-tree.js +57 -47
- package/dist/controller/record-workflow.d.ts +38 -16
- package/dist/controller/record-workflow.js +85 -64
- package/dist/controller/reference-many-controller.js +198 -192
- package/dist/controller/reference-one-controller.d.ts +1 -1
- package/dist/controller/reference-one-controller.js +63 -56
- package/dist/controller/saved-queries.js +104 -91
- package/dist/controller/translate-fallback.js +10 -6
- package/dist/csv/export.d.ts +2 -1
- package/dist/csv/export.js +43 -40
- package/dist/csv/import-controller.js +185 -169
- package/dist/csv/match-headers.js +29 -22
- package/dist/csv/parse-with.js +60 -54
- package/dist/csv/run-import.js +69 -72
- package/dist/csv/validate-rows.d.ts +1 -1
- package/dist/csv/validate-rows.js +65 -52
- package/dist/data/cache-ops.d.ts +5 -4
- package/dist/data/cache-ops.js +208 -190
- package/dist/data/combine-data-providers.js +25 -23
- package/dist/data/data-provider-log.d.ts +45 -0
- package/dist/data/data-provider-log.js +93 -0
- package/dist/data/errors.js +14 -10
- package/dist/data/fake-provider.d.ts +6 -3
- package/dist/data/fake-provider.js +129 -114
- package/dist/data/fetch-utils.js +61 -44
- package/dist/data/filter-branch.d.ts +5 -0
- package/dist/data/filter-branch.js +10 -0
- package/dist/data/get-many-aggregate.js +84 -76
- package/dist/data/http-error.js +17 -12
- package/dist/data/keys.js +50 -36
- package/dist/data/mutation-options.d.ts +14 -9
- package/dist/data/mutation-options.js +93 -109
- package/dist/data/query-options.js +73 -68
- package/dist/data/remove-empty.d.ts +3 -0
- package/dist/data/remove-empty.js +11 -0
- package/dist/data/undoable.d.ts +2 -2
- package/dist/data/undoable.js +190 -174
- package/dist/data/with-lifecycle-callbacks.d.ts +45 -17
- package/dist/data/with-lifecycle-callbacks.js +66 -52
- package/dist/environment/application-update-store.js +58 -55
- package/dist/environment/offline-store.js +21 -21
- package/dist/form/array-binding.d.ts +30 -11
- package/dist/form/array-binding.js +184 -166
- package/dist/form/choice-ids.d.ts +7 -0
- package/dist/form/choice-ids.js +11 -0
- package/dist/form/choices-controller.d.ts +3 -2
- package/dist/form/choices-controller.js +302 -275
- package/dist/form/date-format.d.ts +38 -0
- package/dist/form/date-format.js +89 -0
- package/dist/form/dirty-values.d.ts +2 -2
- package/dist/form/dirty-values.js +22 -11
- package/dist/form/duration.d.ts +22 -0
- package/dist/form/duration.js +49 -0
- package/dist/form/edit-create-form.d.ts +12 -5
- package/dist/form/edit-create-form.js +243 -188
- package/dist/form/event-value.d.ts +6 -0
- package/dist/form/event-value.js +17 -0
- package/dist/form/field-error.d.ts +14 -0
- package/dist/form/field-error.js +28 -0
- package/dist/form/field-name.d.ts +20 -0
- package/dist/form/field-name.js +6 -0
- package/dist/form/filter-form-values.js +63 -49
- package/dist/form/form-controller.d.ts +9 -1
- package/dist/form/form-controller.js +165 -160
- package/dist/form/form-groups.d.ts +4 -3
- package/dist/form/form-groups.js +79 -79
- package/dist/form/form-handle.d.ts +175 -0
- package/dist/form/form-handle.js +68 -0
- package/dist/form/input-binding.d.ts +56 -31
- package/dist/form/input-binding.js +169 -118
- package/dist/form/input-values.d.ts +130 -0
- package/dist/form/input-values.js +177 -0
- package/dist/form/json-text.js +51 -37
- package/dist/form/save-pipeline.d.ts +6 -8
- package/dist/form/save-pipeline.js +91 -82
- package/dist/form/set-form-values.d.ts +6 -0
- package/dist/form/set-form-values.js +10 -0
- package/dist/form/source.js +97 -65
- package/dist/form/text-of.d.ts +2 -0
- package/dist/form/text-of.js +7 -0
- package/dist/form/validators.d.ts +58 -19
- package/dist/form/validators.js +97 -71
- package/dist/geo/bbox.d.ts +5 -4
- package/dist/geo/bbox.js +69 -48
- package/dist/geo/bounds.d.ts +14 -3
- package/dist/geo/bounds.js +98 -68
- package/dist/geo/constraints.js +88 -93
- package/dist/geo/coordinates.js +31 -23
- package/dist/geo/editor.d.ts +13 -13
- package/dist/geo/editor.js +176 -153
- package/dist/geo/geocoding.d.ts +4 -4
- package/dist/geo/geocoding.js +38 -41
- package/dist/geo/geojson.d.ts +12 -74
- package/dist/geo/geojson.js +52 -40
- package/dist/geo/geometry-math.d.ts +4 -4
- package/dist/geo/geometry-math.js +100 -93
- package/dist/geo/index.d.ts +7 -6
- package/dist/geo/map-layers.d.ts +42 -7
- package/dist/geo/map-layers.js +88 -62
- package/dist/geo/map-style.js +31 -32
- package/dist/geo/messages.js +48 -45
- package/dist/geo/nominatim.js +88 -80
- package/dist/geo/osm-presets.d.ts +4 -3
- package/dist/geo/osm-presets.js +108 -86
- package/dist/geo/osm-tags.js +14 -12
- package/dist/geo/overpass.d.ts +14 -2
- package/dist/geo/overpass.js +83 -90
- package/dist/geo/shape-value.d.ts +9 -5
- package/dist/geo/shape-value.js +169 -146
- package/dist/geo/snap-to-roads.d.ts +3 -3
- package/dist/geo/snap-to-roads.js +55 -50
- package/dist/geo/throttle.js +36 -39
- package/dist/guards.d.ts +37 -0
- package/dist/guards.js +62 -0
- package/dist/i18n/default-messages.js +34 -29
- package/dist/i18n/english-messages.js +187 -189
- package/dist/i18n/i18n.d.ts +8 -2
- package/dist/i18n/i18n.js +47 -48
- package/dist/i18n/inflection.d.ts +34 -0
- package/dist/i18n/inflection.js +82 -0
- package/dist/i18n/messages.d.ts +10 -2
- package/dist/i18n/messages.js +17 -4
- package/dist/i18n/missing-keys.d.ts +19 -0
- package/dist/i18n/missing-keys.js +68 -0
- package/dist/i18n/translatable.d.ts +16 -1
- package/dist/i18n/translatable.js +40 -47
- package/dist/index.d.ts +96 -22
- package/dist/index.js +52 -257
- package/dist/json/deep-equal.d.ts +8 -0
- package/dist/json/deep-equal.js +24 -0
- package/dist/json/strip-json-comments.js +38 -42
- package/dist/noop.d.ts +2 -0
- package/dist/noop.js +4 -0
- package/dist/notification/error-message.d.ts +2 -0
- package/dist/notification/error-message.js +10 -0
- package/dist/notification/notification-store.d.ts +3 -2
- package/dist/notification/notification-store.js +41 -45
- package/dist/onboarding-tour/placement.d.ts +34 -0
- package/dist/onboarding-tour/placement.js +67 -0
- package/dist/onboarding-tour/tour-controller.d.ts +25 -0
- package/dist/onboarding-tour/tour-controller.js +50 -0
- package/dist/realtime/add-events-for-mutations.js +75 -81
- package/dist/realtime/announce-lock.d.ts +7 -0
- package/dist/realtime/announce-lock.js +18 -0
- package/dist/realtime/broadcast-channel-transport.d.ts +14 -0
- package/dist/realtime/broadcast-channel-transport.js +69 -0
- package/dist/realtime/copy-members.js +22 -14
- package/dist/realtime/in-memory-lock-provider.js +41 -60
- package/dist/realtime/live.js +95 -101
- package/dist/realtime/lock-options.js +16 -16
- package/dist/realtime/lock-session.js +91 -78
- package/dist/realtime/memory-transport.js +26 -67
- package/dist/realtime/presence.js +120 -122
- package/dist/realtime/realtime-data-provider.d.ts +10 -0
- package/dist/realtime/realtime-data-provider.js +45 -42
- package/dist/realtime/reconnect-backoff.d.ts +15 -0
- package/dist/realtime/reconnect-backoff.js +17 -0
- package/dist/realtime/sse-transport.d.ts +31 -0
- package/dist/realtime/sse-transport.js +235 -0
- package/dist/realtime/status-store.js +23 -28
- package/dist/realtime/topic-hub.d.ts +24 -0
- package/dist/realtime/topic-hub.js +66 -0
- package/dist/realtime/topics.js +10 -12
- package/dist/realtime/types.js +10 -12
- package/dist/realtime/unread-count.js +21 -21
- package/dist/realtime/websocket-transport.d.ts +32 -0
- package/dist/realtime/websocket-transport.js +306 -0
- package/dist/resource/describe-resource.d.ts +2 -0
- package/dist/resource/describe-resource.js +232 -219
- package/dist/resource/get-at-path.js +29 -14
- package/dist/resource/identifier.d.ts +18 -0
- package/dist/resource/identifier.js +27 -0
- package/dist/resource/record.d.ts +4 -0
- package/dist/resource/record.js +10 -0
- package/dist/resource/register.d.ts +7 -0
- package/dist/resource/registry-store.d.ts +45 -15
- package/dist/resource/registry-store.js +67 -60
- package/dist/resource/registry.d.ts +10 -0
- package/dist/resource/registry.js +8 -4
- package/dist/resource/source-path.d.ts +16 -1
- package/dist/rich-text/rich-text-extras.d.ts +132 -0
- package/dist/rich-text/rich-text-extras.js +179 -0
- package/dist/rich-text/rich-text-image.d.ts +124 -0
- package/dist/rich-text/rich-text-image.js +163 -0
- package/dist/rich-text/rich-text.d.ts +62 -0
- package/dist/rich-text/rich-text.js +142 -0
- package/dist/router/location.d.ts +22 -4
- package/dist/router/location.js +41 -18
- package/dist/router/match.js +47 -39
- package/dist/router/paths.js +40 -34
- package/dist/router/record-from-location.d.ts +1 -1
- package/dist/router/record-from-location.js +25 -17
- package/dist/store/adapters.js +51 -55
- package/dist/store/micro-store.js +57 -59
- package/dist/store/shallow-equal.d.ts +12 -0
- package/dist/store/shallow-equal.js +20 -0
- package/dist/store/store.js +54 -57
- package/package.json +13 -11
|
@@ -1,128 +1,126 @@
|
|
|
1
|
+
import { isRecord } from "../guards.js";
|
|
1
2
|
import { createMicroStore } from "../store/micro-store.js";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
3
|
+
// src/realtime/presence.ts
|
|
4
|
+
var toError = (error) => error instanceof Error ? error : new Error(String(error));
|
|
5
|
+
var sameUser = (a, b) => a.id === b.id && a.name === b.name && a.avatar === b.avatar;
|
|
6
|
+
var isPresenceUser = (value) => isRecord(value) && typeof value.id === "string" && typeof value.name === "string";
|
|
7
|
+
/**
|
|
8
|
+
* Announces `user` on `topic` and keeps the other users who announce there.
|
|
9
|
+
* Self is told by `sessionId`, not by user id, so a second tab of the same
|
|
10
|
+
* person still counts. Staleness is measured with `now()` at receipt, never
|
|
11
|
+
* from a time the sender wrote.
|
|
12
|
+
*/
|
|
13
|
+
function createPresenceSession({ dataProvider, topic, user, sessionId = crypto.randomUUID(), heartbeatMs = 15e3, staleMs = 3e4, now = Date.now, onError }) {
|
|
14
|
+
const store = createMicroStore({ users: [] });
|
|
15
|
+
const sessions = /* @__PURE__ */ new Map();
|
|
16
|
+
let running = false;
|
|
17
|
+
let unsubscribe;
|
|
18
|
+
let unsubscribeReconnect;
|
|
19
|
+
let heartbeat;
|
|
20
|
+
let sweep;
|
|
21
|
+
const report = (cause) => {
|
|
22
|
+
try {
|
|
23
|
+
onError?.(toError(cause));
|
|
24
|
+
} catch {}
|
|
25
|
+
};
|
|
26
|
+
const publish = (type, payload) => {
|
|
27
|
+
try {
|
|
28
|
+
dataProvider.publish(topic, {
|
|
29
|
+
type,
|
|
30
|
+
payload
|
|
31
|
+
}).catch(report);
|
|
32
|
+
} catch (cause) {
|
|
33
|
+
report(cause);
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
const announce = () => publish("presence", {
|
|
37
|
+
sessionId,
|
|
38
|
+
user
|
|
39
|
+
});
|
|
40
|
+
const refresh = () => {
|
|
41
|
+
const live = /* @__PURE__ */ new Map();
|
|
42
|
+
for (const entry of sessions.values()) live.set(entry.user.id, entry.user);
|
|
43
|
+
const previous = store.getSnapshot().users;
|
|
44
|
+
const users = previous.flatMap((listed) => live.get(listed.id) ?? []);
|
|
45
|
+
const listedIds = new Set(users.map((listed) => listed.id));
|
|
46
|
+
for (const [id, other] of live) if (!listedIds.has(id)) users.push(other);
|
|
47
|
+
if (!(users.length === previous.length && users.every((other, index) => {
|
|
48
|
+
const prior = previous[index];
|
|
49
|
+
return prior !== void 0 && sameUser(other, prior);
|
|
50
|
+
}))) store.setState({ users });
|
|
51
|
+
};
|
|
52
|
+
const handle = ({ type, payload }) => {
|
|
53
|
+
try {
|
|
54
|
+
if (!isRecord(payload) || typeof payload.sessionId !== "string") return;
|
|
55
|
+
const from = payload.sessionId;
|
|
56
|
+
if (from === sessionId) return;
|
|
57
|
+
if (type === "presence_leave") {
|
|
58
|
+
sessions.delete(from);
|
|
59
|
+
refresh();
|
|
60
|
+
} else if (type === "presence" && isPresenceUser(payload.user)) {
|
|
61
|
+
const known = sessions.has(from);
|
|
62
|
+
sessions.set(from, {
|
|
63
|
+
user: payload.user,
|
|
64
|
+
heardAt: now()
|
|
65
|
+
});
|
|
66
|
+
if (!known) announce();
|
|
67
|
+
refresh();
|
|
68
|
+
}
|
|
69
|
+
} catch (cause) {
|
|
70
|
+
report(cause);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
return {
|
|
74
|
+
store,
|
|
75
|
+
start() {
|
|
76
|
+
if (running) return;
|
|
77
|
+
running = true;
|
|
78
|
+
unsubscribe = dataProvider.subscribe(topic, handle);
|
|
79
|
+
unsubscribeReconnect = dataProvider.onReconnect?.(announce);
|
|
80
|
+
announce();
|
|
81
|
+
heartbeat = setInterval(announce, heartbeatMs);
|
|
82
|
+
sweep = setInterval(() => {
|
|
83
|
+
try {
|
|
84
|
+
const cutoff = now() - staleMs;
|
|
85
|
+
for (const [id, entry] of sessions) if (entry.heardAt < cutoff) sessions.delete(id);
|
|
86
|
+
refresh();
|
|
87
|
+
} catch (cause) {
|
|
88
|
+
report(cause);
|
|
89
|
+
}
|
|
90
|
+
}, Math.min(staleMs / 2, 5e3));
|
|
91
|
+
},
|
|
92
|
+
stop() {
|
|
93
|
+
if (!running) return;
|
|
94
|
+
running = false;
|
|
95
|
+
unsubscribe?.();
|
|
96
|
+
unsubscribe = void 0;
|
|
97
|
+
unsubscribeReconnect?.();
|
|
98
|
+
unsubscribeReconnect = void 0;
|
|
99
|
+
clearInterval(heartbeat);
|
|
100
|
+
clearInterval(sweep);
|
|
101
|
+
heartbeat = sweep = void 0;
|
|
102
|
+
publish("presence_leave", { sessionId });
|
|
103
|
+
sessions.clear();
|
|
104
|
+
try {
|
|
105
|
+
store.setState({ users: [] });
|
|
106
|
+
} catch (cause) {
|
|
107
|
+
report(cause);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
};
|
|
110
111
|
}
|
|
111
112
|
function toPresenceUser(identity) {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
113
|
+
const id = String(identity.id);
|
|
114
|
+
return {
|
|
115
|
+
id,
|
|
116
|
+
name: identity.fullName || id,
|
|
117
|
+
...typeof identity.avatar === "string" ? { avatar: identity.avatar } : {}
|
|
118
|
+
};
|
|
118
119
|
}
|
|
120
|
+
/** The first letters of the first two words, or `?` for a blank name. */
|
|
119
121
|
function getPresenceInitials(name) {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
122
|
+
const words = name.trim().split(/\s+/).filter(Boolean).slice(0, 2);
|
|
123
|
+
if (words.length === 0) return "?";
|
|
124
|
+
return words.map((word) => Array.from(word)[0]).join("").toUpperCase();
|
|
123
125
|
}
|
|
124
|
-
export {
|
|
125
|
-
createPresenceSession,
|
|
126
|
-
getPresenceInitials,
|
|
127
|
-
toPresenceUser
|
|
128
|
-
};
|
|
126
|
+
export { createPresenceSession, getPresenceInitials, toPresenceUser };
|
|
@@ -11,4 +11,14 @@ import type { RealtimeDataProvider, RealtimeDataProviderOptions, RealtimeTranspo
|
|
|
11
11
|
export declare function realtimeDataProvider<TResources extends ResourceMap = ResourceMap>(base: DataProvider<TResources>, transport: RealtimeTransport, { locks }?: RealtimeDataProviderOptions): RealtimeDataProvider<TResources>;
|
|
12
12
|
/** True when `dataProvider` was wrapped with `realtimeDataProvider()`. */
|
|
13
13
|
export declare function isRealtimeDataProvider<TResources extends ResourceMap = ResourceMap>(dataProvider: DataProvider<TResources>): dataProvider is RealtimeDataProvider<TResources>;
|
|
14
|
+
declare const transportMembers: readonly ["onReconnect", "onStatusChange", "getStatus", "onError"];
|
|
15
|
+
/** The optional transport members a data provider may carry without being a full `RealtimeDataProvider`. */
|
|
16
|
+
export type TransportMembers = Pick<RealtimeDataProvider, (typeof transportMembers)[number]>;
|
|
17
|
+
/**
|
|
18
|
+
* True when every transport member `dataProvider` has is a function. Absent
|
|
19
|
+
* members are fine: they are optional, and a provider with only some of them
|
|
20
|
+
* (no `publish`, say) still reports reconnects and status.
|
|
21
|
+
*/
|
|
22
|
+
export declare function hasTransportMembers<TResources extends ResourceMap = ResourceMap>(dataProvider: DataProvider<TResources>): dataProvider is DataProvider<TResources> & TransportMembers;
|
|
14
23
|
export declare function assertRealtimeDataProvider<TResources extends ResourceMap = ResourceMap>(dataProvider: DataProvider<TResources>, caller: string): asserts dataProvider is RealtimeDataProvider<TResources>;
|
|
24
|
+
export {};
|
|
@@ -1,49 +1,52 @@
|
|
|
1
1
|
import { copyMembers } from "./copy-members.js";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
// src/realtime/realtime-data-provider.ts
|
|
3
|
+
var noLockProvider = (method) => Promise.reject(/* @__PURE__ */ new Error(`realtimeDataProvider: ${method}() called but no LockProvider was configured`));
|
|
4
|
+
/**
|
|
5
|
+
* Adds `subscribe`, `publish` and the lock methods to any data provider. The
|
|
6
|
+
* optional transport members are copied only when the transport has them, so
|
|
7
|
+
* `"getStatus" in dataProvider` tells a caller whether it can read a status.
|
|
8
|
+
* Wrap the result of `combineDataProviders`, not the reverse: it keeps only
|
|
9
|
+
* the nine verbs.
|
|
10
|
+
*/
|
|
7
11
|
function realtimeDataProvider(base, transport, { locks } = {}) {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
}
|
|
26
|
-
if (transport.getStatus) {
|
|
27
|
-
provider.getStatus = transport.getStatus.bind(transport);
|
|
28
|
-
}
|
|
29
|
-
if (transport.onError) {
|
|
30
|
-
provider.onError = transport.onError.bind(transport);
|
|
31
|
-
}
|
|
32
|
-
return provider;
|
|
12
|
+
const lockMethods = {
|
|
13
|
+
lock: (resource, params) => locks ? locks.lock(resource, params) : noLockProvider("lock"),
|
|
14
|
+
unlock: (resource, params) => locks ? locks.unlock(resource, params) : noLockProvider("unlock"),
|
|
15
|
+
getLock: (resource, params) => locks ? locks.getLock(resource, params) : noLockProvider("getLock"),
|
|
16
|
+
getLocks: (resource, params) => locks ? locks.getLocks(resource, params) : noLockProvider("getLocks")
|
|
17
|
+
};
|
|
18
|
+
const provider = {
|
|
19
|
+
...copyMembers(base),
|
|
20
|
+
...lockMethods,
|
|
21
|
+
subscribe: (topic, callback) => transport.subscribe(topic, callback),
|
|
22
|
+
publish: (topic, event) => transport.publish(topic, event)
|
|
23
|
+
};
|
|
24
|
+
if (transport.onReconnect) provider.onReconnect = transport.onReconnect.bind(transport);
|
|
25
|
+
if (transport.onStatusChange) provider.onStatusChange = transport.onStatusChange.bind(transport);
|
|
26
|
+
if (transport.getStatus) provider.getStatus = transport.getStatus.bind(transport);
|
|
27
|
+
if (transport.onError) provider.onError = transport.onError.bind(transport);
|
|
28
|
+
return provider;
|
|
33
29
|
}
|
|
30
|
+
/** True when `dataProvider` was wrapped with `realtimeDataProvider()`. */
|
|
34
31
|
function isRealtimeDataProvider(dataProvider) {
|
|
35
|
-
|
|
36
|
-
|
|
32
|
+
const candidate = dataProvider;
|
|
33
|
+
return typeof candidate.subscribe === "function" && typeof candidate.publish === "function";
|
|
34
|
+
}
|
|
35
|
+
var transportMembers = [
|
|
36
|
+
"onReconnect",
|
|
37
|
+
"onStatusChange",
|
|
38
|
+
"getStatus",
|
|
39
|
+
"onError"
|
|
40
|
+
];
|
|
41
|
+
/**
|
|
42
|
+
* True when every transport member `dataProvider` has is a function. Absent
|
|
43
|
+
* members are fine: they are optional, and a provider with only some of them
|
|
44
|
+
* (no `publish`, say) still reports reconnects and status.
|
|
45
|
+
*/
|
|
46
|
+
function hasTransportMembers(dataProvider) {
|
|
47
|
+
return transportMembers.every((name) => !(name in dataProvider) || typeof Reflect.get(dataProvider, name) === "function");
|
|
37
48
|
}
|
|
38
49
|
function assertRealtimeDataProvider(dataProvider, caller) {
|
|
39
|
-
|
|
40
|
-
throw new Error(
|
|
41
|
-
`${caller}: dataProvider does not implement subscribe(). Wrap it with realtimeDataProvider().`
|
|
42
|
-
);
|
|
43
|
-
}
|
|
50
|
+
if (!isRealtimeDataProvider(dataProvider)) throw new Error(`${caller}: dataProvider does not implement subscribe(). Wrap it with realtimeDataProvider().`);
|
|
44
51
|
}
|
|
45
|
-
export {
|
|
46
|
-
assertRealtimeDataProvider,
|
|
47
|
-
isRealtimeDataProvider,
|
|
48
|
-
realtimeDataProvider
|
|
49
|
-
};
|
|
52
|
+
export { assertRealtimeDataProvider, hasTransportMembers, isRealtimeDataProvider, realtimeDataProvider };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface RealtimeReconnectConfig {
|
|
2
|
+
/** Whether a dropped connection is retried. Default `true`. */
|
|
3
|
+
enabled?: boolean;
|
|
4
|
+
/** The wait before the first retry. Default 1000. */
|
|
5
|
+
initialDelayMs?: number;
|
|
6
|
+
/** The wait never grows past this. Default 30000. */
|
|
7
|
+
maxDelayMs?: number;
|
|
8
|
+
/** Retries before giving up. Default `Infinity`. */
|
|
9
|
+
maxAttempts?: number;
|
|
10
|
+
/** Share of each wait that is randomised, up or down. Default 0.3. */
|
|
11
|
+
jitter?: number;
|
|
12
|
+
}
|
|
13
|
+
export declare function resolveReconnectConfig(config?: RealtimeReconnectConfig): Required<RealtimeReconnectConfig>;
|
|
14
|
+
/** The wait before retry number `attempt` (from 0): exponential, capped, then jittered. */
|
|
15
|
+
export declare function reconnectDelay(attempt: number, config: Required<RealtimeReconnectConfig>, random?: () => number): number;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// src/realtime/reconnect-backoff.ts
|
|
2
|
+
function resolveReconnectConfig(config = {}) {
|
|
3
|
+
return {
|
|
4
|
+
enabled: config.enabled ?? true,
|
|
5
|
+
initialDelayMs: config.initialDelayMs ?? 1e3,
|
|
6
|
+
maxDelayMs: config.maxDelayMs ?? 3e4,
|
|
7
|
+
maxAttempts: config.maxAttempts ?? Number.POSITIVE_INFINITY,
|
|
8
|
+
jitter: config.jitter ?? .3
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
/** The wait before retry number `attempt` (from 0): exponential, capped, then jittered. */
|
|
12
|
+
function reconnectDelay(attempt, config, random = Math.random) {
|
|
13
|
+
const base = Math.min(config.initialDelayMs * 2 ** attempt, config.maxDelayMs);
|
|
14
|
+
const spread = base * config.jitter;
|
|
15
|
+
return Math.max(0, base + (random() * 2 - 1) * spread);
|
|
16
|
+
}
|
|
17
|
+
export { reconnectDelay, resolveReconnectConfig };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type RealtimeReconnectConfig } from "./reconnect-backoff.js";
|
|
2
|
+
import type { RealtimeTransport, RealtimeTransportError } from "./types.js";
|
|
3
|
+
export interface SseTransportOptions {
|
|
4
|
+
/** The stream URL; the topic filter and the token are appended to its query. */
|
|
5
|
+
url: string;
|
|
6
|
+
/** Where `publish` POSTs. Without it `publish` rejects. */
|
|
7
|
+
publishUrl?: string;
|
|
8
|
+
/** The bearer token for the stream (as `token`) and for the publish POST. */
|
|
9
|
+
getAuthToken?: () => string | null | undefined | Promise<string | null | undefined>;
|
|
10
|
+
/** Sent to `EventSource` and as `credentials: "include"` on the publish POST. */
|
|
11
|
+
withCredentials?: boolean;
|
|
12
|
+
reconnect?: RealtimeReconnectConfig;
|
|
13
|
+
/** How long the stream stays open with no subscriber. Default 30000. */
|
|
14
|
+
idleDisconnectMs?: number;
|
|
15
|
+
/** The query parameter that carries the topics. Default `"topics"`. */
|
|
16
|
+
topicFilterParam?: string;
|
|
17
|
+
/** Called for every transport error, like a listener from `onError()`. */
|
|
18
|
+
onError?: (error: RealtimeTransportError) => void;
|
|
19
|
+
/** Used for the publish POST. Default the global `fetch`. */
|
|
20
|
+
fetch?: typeof fetch;
|
|
21
|
+
}
|
|
22
|
+
/** A `RealtimeTransport` with every optional member, plus `close()`. */
|
|
23
|
+
export interface SseTransport extends Required<RealtimeTransport> {
|
|
24
|
+
/** Closes the stream and sets `"disconnected"`; a later subscribe opens again. */
|
|
25
|
+
close(): void;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Receives realtime events over server-sent events, one named event per topic,
|
|
29
|
+
* and publishes with a POST.
|
|
30
|
+
*/
|
|
31
|
+
export declare function sseTransport({ url, publishUrl, getAuthToken, withCredentials, reconnect, idleDisconnectMs, topicFilterParam, onError, fetch: fetchOption, }: SseTransportOptions): SseTransport;
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { isRecord } from "../guards.js";
|
|
2
|
+
import { reconnectDelay, resolveReconnectConfig } from "./reconnect-backoff.js";
|
|
3
|
+
import { createTopicHub } from "./topic-hub.js";
|
|
4
|
+
// src/realtime/sse-transport.ts
|
|
5
|
+
function isWireEvent(value) {
|
|
6
|
+
return isRecord(value) && typeof value.type === "string" && (value.meta === void 0 || isRecord(value.meta));
|
|
7
|
+
}
|
|
8
|
+
var DEFAULT_IDLE_DISCONNECT_MS = 3e4;
|
|
9
|
+
/**
|
|
10
|
+
* Receives realtime events over server-sent events, one named event per topic,
|
|
11
|
+
* and publishes with a POST.
|
|
12
|
+
*/
|
|
13
|
+
function sseTransport({ url, publishUrl, getAuthToken, withCredentials = false, reconnect, idleDisconnectMs = DEFAULT_IDLE_DISCONNECT_MS, topicFilterParam = "topics", onError, fetch: fetchOption }) {
|
|
14
|
+
const hub = createTopicHub({ onError });
|
|
15
|
+
hub.setStatus("idle");
|
|
16
|
+
const config = resolveReconnectConfig(reconnect);
|
|
17
|
+
let source = null;
|
|
18
|
+
let filter = /* @__PURE__ */ new Set();
|
|
19
|
+
let active = false;
|
|
20
|
+
let generation = 0;
|
|
21
|
+
let attempt = 0;
|
|
22
|
+
let dropped = false;
|
|
23
|
+
let openQueued = false;
|
|
24
|
+
let resolvingToken = false;
|
|
25
|
+
let retryTimer;
|
|
26
|
+
let idleTimer;
|
|
27
|
+
const readToken = async () => await getAuthToken?.() || void 0;
|
|
28
|
+
const stopStream = () => {
|
|
29
|
+
clearTimeout(retryTimer);
|
|
30
|
+
retryTimer = void 0;
|
|
31
|
+
source?.close();
|
|
32
|
+
source = null;
|
|
33
|
+
filter = /* @__PURE__ */ new Set();
|
|
34
|
+
};
|
|
35
|
+
const streamUrl = (topics, token) => {
|
|
36
|
+
const query = [`${encodeURIComponent(topicFilterParam)}=${encodeURIComponent(topics.join(","))}`, ...token === void 0 ? [] : [`token=${encodeURIComponent(token)}`]].join("&");
|
|
37
|
+
return `${url}${url.includes("?") ? "&" : "?"}${query}`;
|
|
38
|
+
};
|
|
39
|
+
const retryOrGiveUp = (failure) => {
|
|
40
|
+
const retrying = config.enabled && attempt < config.maxAttempts;
|
|
41
|
+
hub.report(failure(retrying));
|
|
42
|
+
if (!retrying) {
|
|
43
|
+
active = false;
|
|
44
|
+
hub.setStatus("disconnected");
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
hub.setStatus("reconnecting");
|
|
48
|
+
retryTimer = setTimeout(() => {
|
|
49
|
+
retryTimer = void 0;
|
|
50
|
+
open(true);
|
|
51
|
+
}, reconnectDelay(attempt, config));
|
|
52
|
+
attempt += 1;
|
|
53
|
+
};
|
|
54
|
+
const handleError = (stream, opened) => {
|
|
55
|
+
if (source !== stream) return;
|
|
56
|
+
stopStream();
|
|
57
|
+
dropped ||= opened;
|
|
58
|
+
retryOrGiveUp((retrying) => ({
|
|
59
|
+
kind: "connect_failed",
|
|
60
|
+
retrying
|
|
61
|
+
}));
|
|
62
|
+
};
|
|
63
|
+
async function open(isRetry = false) {
|
|
64
|
+
stopStream();
|
|
65
|
+
const current = ++generation;
|
|
66
|
+
active = true;
|
|
67
|
+
resolvingToken = true;
|
|
68
|
+
let token;
|
|
69
|
+
try {
|
|
70
|
+
token = await readToken();
|
|
71
|
+
} catch (cause) {
|
|
72
|
+
if (current !== generation) return;
|
|
73
|
+
resolvingToken = false;
|
|
74
|
+
if (isRetry) {
|
|
75
|
+
retryOrGiveUp((retrying) => ({
|
|
76
|
+
kind: "auth_failed",
|
|
77
|
+
cause,
|
|
78
|
+
retrying
|
|
79
|
+
}));
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
active = false;
|
|
83
|
+
hub.report({
|
|
84
|
+
kind: "auth_failed",
|
|
85
|
+
cause,
|
|
86
|
+
retrying: false
|
|
87
|
+
});
|
|
88
|
+
hub.setStatus("disconnected");
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (current !== generation) return;
|
|
92
|
+
resolvingToken = false;
|
|
93
|
+
const topics = hub.topics();
|
|
94
|
+
if (topics.length === 0) {
|
|
95
|
+
goIdle();
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
let stream;
|
|
99
|
+
try {
|
|
100
|
+
stream = new EventSource(streamUrl(topics, token), { withCredentials });
|
|
101
|
+
} catch {
|
|
102
|
+
active = false;
|
|
103
|
+
hub.report({
|
|
104
|
+
kind: "connect_failed",
|
|
105
|
+
retrying: false
|
|
106
|
+
});
|
|
107
|
+
hub.setStatus("disconnected");
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
source = stream;
|
|
111
|
+
filter = new Set(topics);
|
|
112
|
+
let opened = false;
|
|
113
|
+
stream.addEventListener("open", () => {
|
|
114
|
+
if (source !== stream) return;
|
|
115
|
+
opened = true;
|
|
116
|
+
attempt = 0;
|
|
117
|
+
hub.setStatus("connected");
|
|
118
|
+
if (dropped) {
|
|
119
|
+
dropped = false;
|
|
120
|
+
hub.reconnected();
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
stream.addEventListener("error", () => handleError(stream, opened));
|
|
124
|
+
for (const topic of topics) stream.addEventListener(topic, (message) => {
|
|
125
|
+
if (source !== stream || !("data" in message)) return;
|
|
126
|
+
if (typeof message.data === "string") receive(topic, message.data);
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
const receive = (topic, data) => {
|
|
130
|
+
let parsed;
|
|
131
|
+
try {
|
|
132
|
+
parsed = JSON.parse(data);
|
|
133
|
+
} catch (cause) {
|
|
134
|
+
hub.report({
|
|
135
|
+
kind: "parse_failed",
|
|
136
|
+
topic,
|
|
137
|
+
cause,
|
|
138
|
+
retrying: false
|
|
139
|
+
});
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (isWireEvent(parsed)) hub.dispatch({
|
|
143
|
+
...parsed,
|
|
144
|
+
topic
|
|
145
|
+
});
|
|
146
|
+
};
|
|
147
|
+
const queueOpen = () => {
|
|
148
|
+
if (openQueued) return;
|
|
149
|
+
openQueued = true;
|
|
150
|
+
Promise.resolve().then(() => {
|
|
151
|
+
if (!openQueued) return;
|
|
152
|
+
openQueued = false;
|
|
153
|
+
if (!active) attempt = 0;
|
|
154
|
+
return open();
|
|
155
|
+
});
|
|
156
|
+
};
|
|
157
|
+
function goIdle() {
|
|
158
|
+
generation += 1;
|
|
159
|
+
resolvingToken = false;
|
|
160
|
+
clearTimeout(idleTimer);
|
|
161
|
+
idleTimer = void 0;
|
|
162
|
+
stopStream();
|
|
163
|
+
active = false;
|
|
164
|
+
dropped = false;
|
|
165
|
+
if (hub.getStatus() !== "idle") hub.setStatus("idle");
|
|
166
|
+
}
|
|
167
|
+
return {
|
|
168
|
+
subscribe(topic, callback) {
|
|
169
|
+
const stop = hub.subscribe(topic, callback);
|
|
170
|
+
clearTimeout(idleTimer);
|
|
171
|
+
idleTimer = void 0;
|
|
172
|
+
const covered = retryTimer !== void 0 || resolvingToken || filter.has(topic);
|
|
173
|
+
if (!active || !covered) queueOpen();
|
|
174
|
+
return () => {
|
|
175
|
+
stop();
|
|
176
|
+
if (hub.topics().length > 0 || !active) return;
|
|
177
|
+
clearTimeout(idleTimer);
|
|
178
|
+
idleTimer = setTimeout(goIdle, idleDisconnectMs);
|
|
179
|
+
};
|
|
180
|
+
},
|
|
181
|
+
async publish(topic, event) {
|
|
182
|
+
if (!publishUrl) throw new Error("sseTransport needs the publishUrl option to publish");
|
|
183
|
+
const headers = { "Content-Type": "application/json" };
|
|
184
|
+
try {
|
|
185
|
+
const token = await readToken();
|
|
186
|
+
if (token !== void 0) headers.Authorization = `Bearer ${token}`;
|
|
187
|
+
} catch (cause) {
|
|
188
|
+
hub.report({
|
|
189
|
+
kind: "auth_failed",
|
|
190
|
+
topic,
|
|
191
|
+
cause,
|
|
192
|
+
retrying: false
|
|
193
|
+
});
|
|
194
|
+
throw cause;
|
|
195
|
+
}
|
|
196
|
+
const doFetch = fetchOption ?? ((input, init) => globalThis.fetch(input, init));
|
|
197
|
+
try {
|
|
198
|
+
const response = await doFetch(publishUrl, {
|
|
199
|
+
method: "POST",
|
|
200
|
+
headers,
|
|
201
|
+
body: JSON.stringify({
|
|
202
|
+
topic,
|
|
203
|
+
event
|
|
204
|
+
}),
|
|
205
|
+
credentials: withCredentials ? "include" : void 0
|
|
206
|
+
});
|
|
207
|
+
if (!response.ok) throw new Error(`Publishing to ${topic} failed: ${response.status}`);
|
|
208
|
+
} catch (cause) {
|
|
209
|
+
hub.report({
|
|
210
|
+
kind: "send_failed",
|
|
211
|
+
topic,
|
|
212
|
+
cause,
|
|
213
|
+
retrying: false
|
|
214
|
+
});
|
|
215
|
+
throw cause;
|
|
216
|
+
}
|
|
217
|
+
},
|
|
218
|
+
onReconnect: hub.onReconnect,
|
|
219
|
+
onStatusChange: hub.onStatusChange,
|
|
220
|
+
onError: hub.onError,
|
|
221
|
+
getStatus: hub.getStatus,
|
|
222
|
+
close() {
|
|
223
|
+
openQueued = false;
|
|
224
|
+
generation += 1;
|
|
225
|
+
resolvingToken = false;
|
|
226
|
+
clearTimeout(idleTimer);
|
|
227
|
+
idleTimer = void 0;
|
|
228
|
+
stopStream();
|
|
229
|
+
active = false;
|
|
230
|
+
dropped = false;
|
|
231
|
+
hub.setStatus("disconnected");
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
export { sseTransport };
|