@chelone/core 0.6.0 → 0.9.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 +6 -0
- package/dist/assistant/assistant-session.js +204 -181
- package/dist/assistant/echo-transport.js +26 -29
- package/dist/assistant/http-transport.d.ts +10 -0
- package/dist/assistant/http-transport.js +98 -0
- package/dist/assistant/types.d.ts +1 -7
- package/dist/auth/access-control.d.ts +13 -5
- package/dist/auth/access-control.js +25 -21
- package/dist/auth/auth-options.d.ts +23 -17
- package/dist/auth/auth-options.js +82 -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 +64 -0
- package/dist/block-editor/block-document.js +147 -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.js +46 -44
- package/dist/command-menu/hotkey.js +58 -47
- package/dist/command-menu/recents.d.ts +4 -0
- package/dist/command-menu/recents.js +12 -11
- package/dist/command-menu/registry.d.ts +9 -5
- package/dist/command-menu/registry.js +36 -46
- package/dist/controller/calendar-dates.d.ts +12 -0
- package/dist/controller/calendar-dates.js +94 -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 +43 -0
- package/dist/controller/in-place-editor.js +43 -0
- package/dist/controller/infinite-list-controller.js +219 -189
- package/dist/controller/inspector.d.ts +55 -0
- package/dist/controller/inspector.js +70 -0
- package/dist/controller/job-monitor.d.ts +10 -4
- package/dist/controller/job-monitor.js +46 -47
- package/dist/controller/kanban-board.js +45 -24
- package/dist/controller/list-controller.d.ts +8 -5
- package/dist/controller/list-controller.js +447 -400
- package/dist/controller/list-params.js +96 -87
- package/dist/controller/pivot-grid.d.ts +4 -4
- package/dist/controller/pivot-grid.js +89 -95
- 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 +5 -5
- package/dist/controller/record-timeline.js +42 -30
- 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.d.ts +2 -0
- package/dist/controller/reference-many-controller.js +199 -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 +4 -3
- package/dist/csv/export.js +43 -40
- package/dist/csv/import-controller.d.ts +4 -0
- package/dist/csv/import-controller.js +189 -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 -188
- 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 +135 -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 +22 -2
- package/dist/form/choices-controller.js +315 -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 +171 -118
- package/dist/form/input-values.d.ts +140 -0
- package/dist/form/input-values.js +206 -0
- package/dist/form/json-text.d.ts +11 -0
- package/dist/form/json-text.js +62 -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.d.ts +10 -0
- package/dist/geo/coordinates.js +44 -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 +9 -8
- package/dist/geo/map-layers.d.ts +42 -7
- package/dist/geo/map-layers.js +88 -62
- package/dist/geo/map-style.d.ts +12 -0
- package/dist/geo/map-style.js +43 -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 +44 -0
- package/dist/guards.js +77 -0
- package/dist/i18n/default-messages.js +34 -29
- package/dist/i18n/english-messages.d.ts +172 -0
- package/dist/i18n/english-messages.js +359 -189
- package/dist/i18n/i18n.d.ts +2 -2
- package/dist/i18n/i18n.js +47 -60
- 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 +4 -0
- package/dist/i18n/missing-keys.js +70 -62
- package/dist/i18n/translatable.d.ts +16 -1
- package/dist/i18n/translatable.js +40 -47
- package/dist/index.d.ts +87 -39
- package/dist/index.js +54 -301
- 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 +2 -0
- package/dist/onboarding-tour/placement.js +65 -78
- package/dist/onboarding-tour/tour-controller.js +46 -54
- package/dist/realtime/add-events-for-mutations.js +75 -81
- package/dist/realtime/announce-lock.js +16 -13
- package/dist/realtime/broadcast-channel-transport.js +65 -59
- package/dist/realtime/copy-members.js +22 -14
- package/dist/realtime/in-memory-lock-provider.js +40 -48
- 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 +25 -25
- 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.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.js +62 -71
- 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 +247 -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 +23 -2
- package/dist/rich-text/rich-text-extras.d.ts +134 -0
- package/dist/rich-text/rich-text-extras.js +181 -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 +6 -0
- package/dist/rich-text/rich-text.js +125 -88
- package/dist/router/location.d.ts +24 -4
- package/dist/router/location.js +43 -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/dist/ui-defaults.d.ts +15 -0
- package/dist/ui-defaults.js +17 -0
- package/package.json +13 -11
|
@@ -1,82 +1,95 @@
|
|
|
1
1
|
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
|
-
|
|
2
|
+
// src/realtime/lock-session.ts
|
|
3
|
+
var toError = (error) => error instanceof Error ? error : new Error(String(error));
|
|
4
|
+
/**
|
|
5
|
+
* Takes a record's lock on `start()` and gives it back on `stop()`. Steps run
|
|
6
|
+
* one after another, so a `stop()` that arrives while `lock` is in flight
|
|
7
|
+
* releases the lock once it lands, and `start()`, `stop()`, `start()` in one
|
|
8
|
+
* tick ends holding it. Locks carry no expiry: a crashed tab leaves its lock
|
|
9
|
+
* until the lock provider drops it.
|
|
10
|
+
*/
|
|
11
|
+
function createLockSession({ dataProvider, resource, id, getIdentity, meta, onError }) {
|
|
12
|
+
const store = createMicroStore({
|
|
13
|
+
lock: null,
|
|
14
|
+
isLocking: false,
|
|
15
|
+
error: null
|
|
16
|
+
});
|
|
17
|
+
const metaParam = meta ? { meta } : {};
|
|
18
|
+
let held = null;
|
|
19
|
+
let running = false;
|
|
20
|
+
let epoch = 0;
|
|
21
|
+
let queue = Promise.resolve();
|
|
22
|
+
const enqueue = (step) => {
|
|
23
|
+
queue = queue.then(step).catch(() => {});
|
|
24
|
+
return queue;
|
|
25
|
+
};
|
|
26
|
+
const acquire = async (myEpoch) => {
|
|
27
|
+
const isCurrent = () => epoch === myEpoch;
|
|
28
|
+
try {
|
|
29
|
+
const identity = await getIdentity();
|
|
30
|
+
const lock = await dataProvider.lock(resource, {
|
|
31
|
+
id,
|
|
32
|
+
identity,
|
|
33
|
+
...metaParam
|
|
34
|
+
});
|
|
35
|
+
held = { identity };
|
|
36
|
+
if (isCurrent()) store.setState({
|
|
37
|
+
lock,
|
|
38
|
+
isLocking: false
|
|
39
|
+
});
|
|
40
|
+
} catch (cause) {
|
|
41
|
+
if (!isCurrent()) return;
|
|
42
|
+
const error = toError(cause);
|
|
43
|
+
store.setState({
|
|
44
|
+
isLocking: false,
|
|
45
|
+
error
|
|
46
|
+
});
|
|
47
|
+
onError?.(error);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
const release = async () => {
|
|
51
|
+
if (!held) return;
|
|
52
|
+
const { identity } = held;
|
|
53
|
+
held = null;
|
|
54
|
+
try {
|
|
55
|
+
await dataProvider.unlock(resource, {
|
|
56
|
+
id,
|
|
57
|
+
identity,
|
|
58
|
+
...metaParam
|
|
59
|
+
});
|
|
60
|
+
} catch (cause) {
|
|
61
|
+
onError?.(toError(cause));
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
return {
|
|
65
|
+
store,
|
|
66
|
+
start() {
|
|
67
|
+
if (running) return queue;
|
|
68
|
+
running = true;
|
|
69
|
+
const myEpoch = ++epoch;
|
|
70
|
+
store.setState({
|
|
71
|
+
isLocking: true,
|
|
72
|
+
error: null
|
|
73
|
+
});
|
|
74
|
+
return enqueue(() => acquire(myEpoch));
|
|
75
|
+
},
|
|
76
|
+
stop() {
|
|
77
|
+
if (!running) return queue;
|
|
78
|
+
running = false;
|
|
79
|
+
epoch++;
|
|
80
|
+
store.setState({
|
|
81
|
+
lock: null,
|
|
82
|
+
isLocking: false,
|
|
83
|
+
error: null
|
|
84
|
+
});
|
|
85
|
+
return enqueue(release);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
71
88
|
}
|
|
89
|
+
/** The explicit identity wins; otherwise the auth provider's. */
|
|
72
90
|
async function resolveLockIdentity(authProvider, explicit) {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
"no identity available. Pass identity explicitly or configure an authProvider."
|
|
77
|
-
);
|
|
91
|
+
if (explicit !== void 0) return explicit;
|
|
92
|
+
if (authProvider?.getIdentity) return (await authProvider.getIdentity()).id;
|
|
93
|
+
throw new Error("no identity available. Pass identity explicitly or configure an authProvider.");
|
|
78
94
|
}
|
|
79
|
-
export {
|
|
80
|
-
createLockSession,
|
|
81
|
-
resolveLockIdentity
|
|
82
|
-
};
|
|
95
|
+
export { createLockSession, resolveLockIdentity };
|
|
@@ -1,27 +1,27 @@
|
|
|
1
1
|
import { createTopicHub } from "./topic-hub.js";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
2
|
+
// src/realtime/memory-transport.ts
|
|
3
|
+
/** Routes events by topic inside one process, with no server or client library. */
|
|
4
|
+
function memoryTransport({ delayMs, onError } = {}) {
|
|
5
|
+
const hub = createTopicHub({ onError });
|
|
6
|
+
const publishedEvents = [];
|
|
7
|
+
return {
|
|
8
|
+
publishedEvents,
|
|
9
|
+
subscribe: hub.subscribe,
|
|
10
|
+
async publish(topic, event) {
|
|
11
|
+
const full = {
|
|
12
|
+
topic,
|
|
13
|
+
...event
|
|
14
|
+
};
|
|
15
|
+
publishedEvents.push(full);
|
|
16
|
+
if (delayMs) await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
17
|
+
hub.dispatch(full);
|
|
18
|
+
},
|
|
19
|
+
onReconnect: hub.onReconnect,
|
|
20
|
+
onStatusChange: hub.onStatusChange,
|
|
21
|
+
onError: hub.onError,
|
|
22
|
+
getStatus: hub.getStatus,
|
|
23
|
+
simulateReconnect: hub.reconnected,
|
|
24
|
+
setStatus: hub.setStatus
|
|
25
|
+
};
|
|
24
26
|
}
|
|
25
|
-
export {
|
|
26
|
-
memoryTransport
|
|
27
|
-
};
|
|
27
|
+
export { memoryTransport };
|
|
@@ -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,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;
|