@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,33 +1,28 @@
|
|
|
1
1
|
import { createMicroStore } from "../store/micro-store.js";
|
|
2
|
+
// src/realtime/status-store.ts
|
|
2
3
|
function readStatus(source) {
|
|
3
|
-
|
|
4
|
-
|
|
4
|
+
if (source.getStatus) return source.getStatus();
|
|
5
|
+
return source.onStatusChange ? "disconnected" : "idle";
|
|
5
6
|
}
|
|
7
|
+
/** Follows the connection while it has a subscriber, and lets go of it at zero. */
|
|
6
8
|
function createRealtimeStatusStore(source) {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
);
|
|
29
|
-
return store;
|
|
9
|
+
const store = createMicroStore({
|
|
10
|
+
status: readStatus(source),
|
|
11
|
+
lastError: null
|
|
12
|
+
}, { onSubscribed() {
|
|
13
|
+
const status = readStatus(source);
|
|
14
|
+
store.setState(status === "connected" ? {
|
|
15
|
+
status,
|
|
16
|
+
lastError: null
|
|
17
|
+
} : { status });
|
|
18
|
+
const stops = [source.onStatusChange?.((status) => store.setState(status === "connected" ? {
|
|
19
|
+
status,
|
|
20
|
+
lastError: null
|
|
21
|
+
} : { status })), source.onError?.((lastError) => store.setState({ lastError }))];
|
|
22
|
+
return () => {
|
|
23
|
+
for (const stop of stops) stop?.();
|
|
24
|
+
};
|
|
25
|
+
} });
|
|
26
|
+
return store;
|
|
30
27
|
}
|
|
31
|
-
export {
|
|
32
|
-
createRealtimeStatusStore
|
|
33
|
-
};
|
|
28
|
+
export { createRealtimeStatusStore };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { RealtimeConnectionStatus, RealtimeEvent, RealtimeTransport, RealtimeTransportError } from "./types.js";
|
|
2
|
+
export interface TopicHubOptions {
|
|
3
|
+
/** Called for every reported error, like a listener from `onError()`. */
|
|
4
|
+
onError?: (error: RealtimeTransportError) => void;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* The subscriber bookkeeping every transport needs: per-topic subscriptions,
|
|
8
|
+
* isolated dispatch, and the status, reconnect and error listeners.
|
|
9
|
+
*/
|
|
10
|
+
export interface TopicHub extends Required<Pick<RealtimeTransport, "subscribe" | "onReconnect" | "onStatusChange" | "getStatus" | "onError">> {
|
|
11
|
+
/** Calls the subscribers of `event.topic`; a throwing one is reported. */
|
|
12
|
+
dispatch(event: RealtimeEvent): void;
|
|
13
|
+
/** Whether any subscriber is listening on `topic`. */
|
|
14
|
+
has(topic: string): boolean;
|
|
15
|
+
/** Every topic with a subscriber, in the order each was first subscribed. */
|
|
16
|
+
topics(): string[];
|
|
17
|
+
/** Calls the `onError` option and every `onError` listener. */
|
|
18
|
+
report(error: RealtimeTransportError): void;
|
|
19
|
+
setStatus(status: RealtimeConnectionStatus): void;
|
|
20
|
+
/** Calls every `onReconnect` listener. */
|
|
21
|
+
reconnected(): void;
|
|
22
|
+
}
|
|
23
|
+
/** Starts `"connected"`; a transport that connects asynchronously sets its own status. */
|
|
24
|
+
export declare function createTopicHub({ onError }?: TopicHubOptions): TopicHub;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// src/realtime/topic-hub.ts
|
|
2
|
+
var addListener = (listeners, listener) => {
|
|
3
|
+
listeners.add(listener);
|
|
4
|
+
return () => {
|
|
5
|
+
listeners.delete(listener);
|
|
6
|
+
};
|
|
7
|
+
};
|
|
8
|
+
var isolate = (call) => {
|
|
9
|
+
try {
|
|
10
|
+
call();
|
|
11
|
+
} catch {}
|
|
12
|
+
};
|
|
13
|
+
/** Starts `"connected"`; a transport that connects asynchronously sets its own status. */
|
|
14
|
+
function createTopicHub({ onError } = {}) {
|
|
15
|
+
const subscribers = /* @__PURE__ */ new Map();
|
|
16
|
+
const reconnectListeners = /* @__PURE__ */ new Set();
|
|
17
|
+
const statusListeners = /* @__PURE__ */ new Set();
|
|
18
|
+
const errorListeners = /* @__PURE__ */ new Set();
|
|
19
|
+
let status = "connected";
|
|
20
|
+
const report = (error) => {
|
|
21
|
+
for (const listener of [...onError ? [onError] : [], ...errorListeners]) isolate(() => listener(error));
|
|
22
|
+
};
|
|
23
|
+
return {
|
|
24
|
+
subscribe(topic, callback) {
|
|
25
|
+
const subscription = { callback };
|
|
26
|
+
const set = subscribers.get(topic) ?? /* @__PURE__ */ new Set();
|
|
27
|
+
set.add(subscription);
|
|
28
|
+
subscribers.set(topic, set);
|
|
29
|
+
return () => {
|
|
30
|
+
set.delete(subscription);
|
|
31
|
+
if (set.size === 0 && subscribers.get(topic) === set) subscribers.delete(topic);
|
|
32
|
+
};
|
|
33
|
+
},
|
|
34
|
+
dispatch(event) {
|
|
35
|
+
const live = subscribers.get(event.topic);
|
|
36
|
+
for (const subscription of [...live ?? []]) {
|
|
37
|
+
if (!live?.has(subscription)) continue;
|
|
38
|
+
try {
|
|
39
|
+
subscription.callback(event);
|
|
40
|
+
} catch (cause) {
|
|
41
|
+
report({
|
|
42
|
+
kind: "handler_threw",
|
|
43
|
+
topic: event.topic,
|
|
44
|
+
cause,
|
|
45
|
+
retrying: false
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
has: (topic) => subscribers.has(topic),
|
|
51
|
+
topics: () => [...subscribers.keys()],
|
|
52
|
+
report,
|
|
53
|
+
onReconnect: (callback) => addListener(reconnectListeners, callback),
|
|
54
|
+
onStatusChange: (callback) => addListener(statusListeners, callback),
|
|
55
|
+
onError: (callback) => addListener(errorListeners, callback),
|
|
56
|
+
getStatus: () => status,
|
|
57
|
+
setStatus(next) {
|
|
58
|
+
status = next;
|
|
59
|
+
for (const listener of [...statusListeners]) isolate(() => listener(next));
|
|
60
|
+
},
|
|
61
|
+
reconnected() {
|
|
62
|
+
for (const listener of [...reconnectListeners]) isolate(listener);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
export { createTopicHub };
|
package/dist/realtime/topics.js
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
resourceTopic
|
|
12
|
-
};
|
|
1
|
+
// src/realtime/topics.ts
|
|
2
|
+
/** Events for the resource as a whole: creates, and the ids of any write. */
|
|
3
|
+
var resourceTopic = (resource) => `resource/${resource}`;
|
|
4
|
+
/** Events for one record. `1` and `"1"` give the same topic. */
|
|
5
|
+
var recordTopic = (resource, id) => `resource/${resource}/${id}`;
|
|
6
|
+
var lockResourceTopic = (resource) => `lock/${resource}`;
|
|
7
|
+
var lockTopic = (resource, id) => `lock/${resource}/${id}`;
|
|
8
|
+
/** Who is on one record. `1` and `"1"` give the same topic. */
|
|
9
|
+
var presenceTopic = (resource, id) => `presence/${resource}/${id}`;
|
|
10
|
+
export { lockResourceTopic, lockTopic, presenceTopic, recordTopic, resourceTopic };
|
package/dist/realtime/types.js
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
}
|
|
11
|
-
export {
|
|
12
|
-
LockConflictError
|
|
1
|
+
// src/realtime/types.ts
|
|
2
|
+
/** Rejected by `lock()` when another identity already holds the record. */
|
|
3
|
+
var LockConflictError = class extends Error {
|
|
4
|
+
kind = "LockConflictError";
|
|
5
|
+
existingLock;
|
|
6
|
+
constructor(existingLock) {
|
|
7
|
+
super(`Resource ${existingLock.resource}/${String(existingLock.recordId)} is locked by ${String(existingLock.identity)}`);
|
|
8
|
+
this.existingLock = existingLock;
|
|
9
|
+
}
|
|
13
10
|
};
|
|
11
|
+
export { LockConflictError };
|
|
@@ -1,27 +1,27 @@
|
|
|
1
1
|
import { resourceTopic } from "./topics.js";
|
|
2
|
-
|
|
2
|
+
// src/realtime/unread-count.ts
|
|
3
|
+
var DEFAULT_EVENTS = ["created"];
|
|
4
|
+
/**
|
|
5
|
+
* The store key of a resource's unread count for an event list. The default
|
|
6
|
+
* `["created"]` keeps `realtime.unread.<resource>`; any other list adds its
|
|
7
|
+
* sorted, de-duplicated types as a suffix, so the order of `events` never
|
|
8
|
+
* splits a count and links watching different events never share one.
|
|
9
|
+
*/
|
|
3
10
|
function unreadKey(resource, events = DEFAULT_EVENTS) {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
11
|
+
const types = [...new Set(events)].sort();
|
|
12
|
+
const base = `realtime.unread.${resource}`;
|
|
13
|
+
return types.length === 1 && types[0] === "created" ? base : `${base}.${types.join("+")}`;
|
|
7
14
|
}
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
return dataProvider.subscribe(resourceTopic(resource), (event) => {
|
|
16
|
-
if (!events.includes(event.type)) return;
|
|
17
|
-
store.setItem(key, (store.getItem(key) ?? 0) + 1);
|
|
18
|
-
});
|
|
15
|
+
/** Adds one to the count under `unreadKey(resource, events)` for each listed event. Returns the teardown. */
|
|
16
|
+
function subscribeUnreadCount({ dataProvider, store, resource, events = DEFAULT_EVENTS }) {
|
|
17
|
+
const key = unreadKey(resource, events);
|
|
18
|
+
return dataProvider.subscribe(resourceTopic(resource), (event) => {
|
|
19
|
+
if (!events.includes(event.type)) return;
|
|
20
|
+
store.setItem(key, (store.getItem(key) ?? 0) + 1);
|
|
21
|
+
});
|
|
19
22
|
}
|
|
23
|
+
/** Resets the count of one event list; pass the same `events` the link subscribed with. */
|
|
20
24
|
function clearUnread(store, resource, events) {
|
|
21
|
-
|
|
25
|
+
store.setItem(unreadKey(resource, events), 0);
|
|
22
26
|
}
|
|
23
|
-
export {
|
|
24
|
-
clearUnread,
|
|
25
|
-
subscribeUnreadCount,
|
|
26
|
-
unreadKey
|
|
27
|
-
};
|
|
27
|
+
export { clearUnread, subscribeUnreadCount, unreadKey };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { type RealtimeReconnectConfig } from "./reconnect-backoff.js";
|
|
2
|
+
import type { RealtimeTransport, RealtimeTransportError } from "./types.js";
|
|
3
|
+
export interface WebSocketTransportOptions {
|
|
4
|
+
url: string;
|
|
5
|
+
/** Subprotocols offered on every connection. */
|
|
6
|
+
protocols?: string[];
|
|
7
|
+
/** Read before every connection, so a retry carries a fresh token. */
|
|
8
|
+
getAuthToken?: () => string | null | undefined | Promise<string | null | undefined>;
|
|
9
|
+
/**
|
|
10
|
+
* Where the token goes. `"query"` (default) adds a URL-encoded `token`
|
|
11
|
+
* parameter; `"subprotocol"` offers it after `protocols` and keeps it out of
|
|
12
|
+
* the URL.
|
|
13
|
+
*/
|
|
14
|
+
authMode?: "query" | "subprotocol";
|
|
15
|
+
reconnect?: RealtimeReconnectConfig;
|
|
16
|
+
/** Close the socket after this long with no subscriber. Default 30000. */
|
|
17
|
+
idleDisconnectMs?: number;
|
|
18
|
+
/** Ping this often; a ping with no reply by the next one is a drop. Default 30000. */
|
|
19
|
+
heartbeatMs?: number;
|
|
20
|
+
/** Called for every transport error, like a listener from `onError()`. */
|
|
21
|
+
onError?: (error: RealtimeTransportError) => void;
|
|
22
|
+
}
|
|
23
|
+
/** A `RealtimeTransport` with every optional member, plus `close()`. */
|
|
24
|
+
export interface WebSocketTransport extends Required<RealtimeTransport> {
|
|
25
|
+
/**
|
|
26
|
+
* Closes the socket, rejects queued publishes and sets `"disconnected"`.
|
|
27
|
+
* A later subscribe or publish opens again.
|
|
28
|
+
*/
|
|
29
|
+
close(): void;
|
|
30
|
+
}
|
|
31
|
+
/** Carries realtime events over one WebSocket and reconnects with backoff. */
|
|
32
|
+
export declare function webSocketTransport({ url, protocols, getAuthToken, authMode, reconnect, idleDisconnectMs, heartbeatMs, onError, }: WebSocketTransportOptions): WebSocketTransport;
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import { reconnectDelay, resolveReconnectConfig } from "./reconnect-backoff.js";
|
|
2
|
+
import { createTopicHub } from "./topic-hub.js";
|
|
3
|
+
// src/realtime/websocket-transport.ts
|
|
4
|
+
var isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5
|
+
function isServerFrame(value) {
|
|
6
|
+
if (!isObject(value)) return false;
|
|
7
|
+
if (!("topic" in value) || typeof value.topic !== "string") return false;
|
|
8
|
+
if (!("type" in value) || typeof value.type !== "string") return false;
|
|
9
|
+
return !("meta" in value) || value.meta === void 0 || isObject(value.meta);
|
|
10
|
+
}
|
|
11
|
+
var isPong = (value) => isObject(value) && "op" in value && value.op === "pong";
|
|
12
|
+
function parseFrame(data) {
|
|
13
|
+
try {
|
|
14
|
+
if (typeof data !== "string") throw new Error("frame is not text");
|
|
15
|
+
const frame = JSON.parse(data);
|
|
16
|
+
if (isPong(frame)) return { kind: "pong" };
|
|
17
|
+
if (isServerFrame(frame)) return {
|
|
18
|
+
kind: "event",
|
|
19
|
+
event: frame
|
|
20
|
+
};
|
|
21
|
+
throw new Error("frame is not a realtime event");
|
|
22
|
+
} catch (cause) {
|
|
23
|
+
return {
|
|
24
|
+
kind: "invalid",
|
|
25
|
+
cause: cause instanceof Error ? cause : /* @__PURE__ */ new Error("frame is invalid")
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/** Carries realtime events over one WebSocket and reconnects with backoff. */
|
|
30
|
+
function webSocketTransport({ url, protocols = [], getAuthToken, authMode = "query", reconnect, idleDisconnectMs = 3e4, heartbeatMs = 3e4, onError }) {
|
|
31
|
+
const hub = createTopicHub({ onError });
|
|
32
|
+
const backoff = resolveReconnectConfig(reconnect);
|
|
33
|
+
hub.setStatus("idle");
|
|
34
|
+
let connection = null;
|
|
35
|
+
let connecting = false;
|
|
36
|
+
let epoch = 0;
|
|
37
|
+
let attempt = 0;
|
|
38
|
+
let lostConnection = false;
|
|
39
|
+
let retryTimer;
|
|
40
|
+
let idleTimer;
|
|
41
|
+
let heartbeatTimer;
|
|
42
|
+
let queue = [];
|
|
43
|
+
const setStatus = (status) => {
|
|
44
|
+
if (hub.getStatus() !== status) hub.setStatus(status);
|
|
45
|
+
};
|
|
46
|
+
const rejectQueue = (reason) => {
|
|
47
|
+
const rejected = queue;
|
|
48
|
+
queue = [];
|
|
49
|
+
for (const publish of rejected) publish.reject(reason);
|
|
50
|
+
};
|
|
51
|
+
const send = (target, frame, topic) => {
|
|
52
|
+
try {
|
|
53
|
+
target.socket.send(JSON.stringify(frame));
|
|
54
|
+
return null;
|
|
55
|
+
} catch (cause) {
|
|
56
|
+
hub.report({
|
|
57
|
+
kind: "send_failed",
|
|
58
|
+
topic,
|
|
59
|
+
cause,
|
|
60
|
+
retrying: false
|
|
61
|
+
});
|
|
62
|
+
return cause instanceof Error ? cause : /* @__PURE__ */ new Error("send failed");
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
const stopHeartbeat = () => {
|
|
66
|
+
clearInterval(heartbeatTimer);
|
|
67
|
+
heartbeatTimer = void 0;
|
|
68
|
+
};
|
|
69
|
+
const detach = (target) => {
|
|
70
|
+
stopHeartbeat();
|
|
71
|
+
target.socket.removeEventListener("open", target.onOpen);
|
|
72
|
+
target.socket.removeEventListener("message", target.onMessage);
|
|
73
|
+
target.socket.removeEventListener("close", target.onClose);
|
|
74
|
+
try {
|
|
75
|
+
target.socket.close();
|
|
76
|
+
} catch {}
|
|
77
|
+
if (connection === target) connection = null;
|
|
78
|
+
};
|
|
79
|
+
const teardown = () => {
|
|
80
|
+
epoch += 1;
|
|
81
|
+
connecting = false;
|
|
82
|
+
attempt = 0;
|
|
83
|
+
lostConnection = false;
|
|
84
|
+
clearTimeout(retryTimer);
|
|
85
|
+
retryTimer = void 0;
|
|
86
|
+
clearTimeout(idleTimer);
|
|
87
|
+
idleTimer = void 0;
|
|
88
|
+
if (connection) detach(connection);
|
|
89
|
+
};
|
|
90
|
+
const touchIdle = () => {
|
|
91
|
+
clearTimeout(idleTimer);
|
|
92
|
+
idleTimer = void 0;
|
|
93
|
+
if (hub.topics().length > 0) return;
|
|
94
|
+
idleTimer = setTimeout(() => {
|
|
95
|
+
idleTimer = void 0;
|
|
96
|
+
if (hub.topics().length > 0) return;
|
|
97
|
+
if (!connection && !connecting && retryTimer === void 0) return;
|
|
98
|
+
teardown();
|
|
99
|
+
rejectQueue(/* @__PURE__ */ new Error("webSocketTransport went idle"));
|
|
100
|
+
setStatus("idle");
|
|
101
|
+
}, idleDisconnectMs);
|
|
102
|
+
};
|
|
103
|
+
const giveUp = (reason) => {
|
|
104
|
+
rejectQueue(reason);
|
|
105
|
+
setStatus("disconnected");
|
|
106
|
+
};
|
|
107
|
+
const lose = (target, clean, cause) => {
|
|
108
|
+
detach(target);
|
|
109
|
+
clearTimeout(idleTimer);
|
|
110
|
+
if (target.opened) lostConnection = true;
|
|
111
|
+
if (clean) {
|
|
112
|
+
setStatus("disconnected");
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (backoff.enabled && attempt < backoff.maxAttempts) {
|
|
116
|
+
const delay = reconnectDelay(attempt, backoff);
|
|
117
|
+
attempt += 1;
|
|
118
|
+
hub.report({
|
|
119
|
+
kind: "connect_failed",
|
|
120
|
+
cause,
|
|
121
|
+
retrying: true
|
|
122
|
+
});
|
|
123
|
+
setStatus("reconnecting");
|
|
124
|
+
retryTimer = setTimeout(() => {
|
|
125
|
+
retryTimer = void 0;
|
|
126
|
+
connect();
|
|
127
|
+
}, delay);
|
|
128
|
+
touchIdle();
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
hub.report({
|
|
132
|
+
kind: "connect_failed",
|
|
133
|
+
cause,
|
|
134
|
+
retrying: false
|
|
135
|
+
});
|
|
136
|
+
giveUp(/* @__PURE__ */ new Error("webSocketTransport could not connect"));
|
|
137
|
+
};
|
|
138
|
+
const heartbeat = (target) => {
|
|
139
|
+
if (target.awaitingPong) {
|
|
140
|
+
lose(target, false, { reason: "heartbeat missed" });
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
target.awaitingPong = true;
|
|
144
|
+
send(target, { op: "ping" });
|
|
145
|
+
};
|
|
146
|
+
const handleMessage = (target, message) => {
|
|
147
|
+
target.awaitingPong = false;
|
|
148
|
+
const parsed = parseFrame(message.data);
|
|
149
|
+
if (parsed.kind === "pong") return;
|
|
150
|
+
if (parsed.kind === "invalid") {
|
|
151
|
+
hub.report({
|
|
152
|
+
kind: "parse_failed",
|
|
153
|
+
cause: parsed.cause,
|
|
154
|
+
retrying: false
|
|
155
|
+
});
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
const { topic, type, payload, meta } = parsed.event;
|
|
159
|
+
hub.dispatch({
|
|
160
|
+
topic,
|
|
161
|
+
type,
|
|
162
|
+
payload,
|
|
163
|
+
...meta ? { meta } : {}
|
|
164
|
+
});
|
|
165
|
+
};
|
|
166
|
+
const handleOpen = (target) => {
|
|
167
|
+
target.opened = true;
|
|
168
|
+
attempt = 0;
|
|
169
|
+
setStatus("connected");
|
|
170
|
+
for (const topic of hub.topics()) send(target, {
|
|
171
|
+
op: "subscribe",
|
|
172
|
+
topic
|
|
173
|
+
});
|
|
174
|
+
const flushed = queue;
|
|
175
|
+
queue = [];
|
|
176
|
+
for (const { topic, event, resolve, reject } of flushed) {
|
|
177
|
+
const failure = send(target, {
|
|
178
|
+
op: "publish",
|
|
179
|
+
topic,
|
|
180
|
+
event
|
|
181
|
+
}, topic);
|
|
182
|
+
if (failure) reject(failure);
|
|
183
|
+
else resolve();
|
|
184
|
+
}
|
|
185
|
+
heartbeatTimer = setInterval(() => heartbeat(target), heartbeatMs);
|
|
186
|
+
touchIdle();
|
|
187
|
+
if (lostConnection) {
|
|
188
|
+
lostConnection = false;
|
|
189
|
+
hub.reconnected();
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
const open = (token) => {
|
|
193
|
+
let target;
|
|
194
|
+
try {
|
|
195
|
+
const address = token && authMode === "query" ? `${url}${url.includes("?") ? "&" : "?"}token=${encodeURIComponent(token)}` : url;
|
|
196
|
+
const offered = token && authMode === "subprotocol" ? [...protocols, token] : protocols;
|
|
197
|
+
target = {
|
|
198
|
+
socket: offered.length > 0 ? new WebSocket(address, offered) : new WebSocket(address),
|
|
199
|
+
opened: false,
|
|
200
|
+
awaitingPong: false,
|
|
201
|
+
onOpen: () => handleOpen(target),
|
|
202
|
+
onMessage: (message) => handleMessage(target, message),
|
|
203
|
+
onClose: (event) => lose(target, event.wasClean, {
|
|
204
|
+
code: event.code,
|
|
205
|
+
reason: event.reason
|
|
206
|
+
})
|
|
207
|
+
};
|
|
208
|
+
} catch (error) {
|
|
209
|
+
const name = error instanceof Error ? error.name : "Error";
|
|
210
|
+
const cause = /* @__PURE__ */ new Error(`WebSocket constructor failed: ${name}`);
|
|
211
|
+
hub.report({
|
|
212
|
+
kind: "connect_failed",
|
|
213
|
+
cause,
|
|
214
|
+
retrying: false
|
|
215
|
+
});
|
|
216
|
+
giveUp(cause);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
connection = target;
|
|
220
|
+
target.socket.addEventListener("open", target.onOpen);
|
|
221
|
+
target.socket.addEventListener("message", target.onMessage);
|
|
222
|
+
target.socket.addEventListener("close", target.onClose);
|
|
223
|
+
};
|
|
224
|
+
async function connect() {
|
|
225
|
+
connecting = true;
|
|
226
|
+
const startedAt = epoch;
|
|
227
|
+
let token;
|
|
228
|
+
if (getAuthToken) {
|
|
229
|
+
try {
|
|
230
|
+
token = await getAuthToken();
|
|
231
|
+
} catch (cause) {
|
|
232
|
+
if (epoch !== startedAt) return;
|
|
233
|
+
connecting = false;
|
|
234
|
+
hub.report({
|
|
235
|
+
kind: "auth_failed",
|
|
236
|
+
cause,
|
|
237
|
+
retrying: false
|
|
238
|
+
});
|
|
239
|
+
giveUp(cause instanceof Error ? cause : /* @__PURE__ */ new Error("auth failed"));
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
if (epoch !== startedAt) return;
|
|
243
|
+
}
|
|
244
|
+
connecting = false;
|
|
245
|
+
open(token);
|
|
246
|
+
}
|
|
247
|
+
const ensureOpen = () => {
|
|
248
|
+
if (connection || connecting || retryTimer !== void 0) return;
|
|
249
|
+
attempt = 0;
|
|
250
|
+
connect();
|
|
251
|
+
};
|
|
252
|
+
return {
|
|
253
|
+
subscribe(topic, callback) {
|
|
254
|
+
const first = !hub.has(topic);
|
|
255
|
+
const unsubscribe = hub.subscribe(topic, callback);
|
|
256
|
+
touchIdle();
|
|
257
|
+
if (first && connection?.opened) send(connection, {
|
|
258
|
+
op: "subscribe",
|
|
259
|
+
topic
|
|
260
|
+
}, topic);
|
|
261
|
+
ensureOpen();
|
|
262
|
+
let done = false;
|
|
263
|
+
return () => {
|
|
264
|
+
if (done) return;
|
|
265
|
+
done = true;
|
|
266
|
+
unsubscribe();
|
|
267
|
+
if (!hub.has(topic) && connection?.opened) send(connection, {
|
|
268
|
+
op: "unsubscribe",
|
|
269
|
+
topic
|
|
270
|
+
}, topic);
|
|
271
|
+
touchIdle();
|
|
272
|
+
};
|
|
273
|
+
},
|
|
274
|
+
publish(topic, event) {
|
|
275
|
+
if (connection?.opened) {
|
|
276
|
+
touchIdle();
|
|
277
|
+
const failure = send(connection, {
|
|
278
|
+
op: "publish",
|
|
279
|
+
topic,
|
|
280
|
+
event
|
|
281
|
+
}, topic);
|
|
282
|
+
return failure ? Promise.reject(failure) : Promise.resolve();
|
|
283
|
+
}
|
|
284
|
+
return new Promise((resolve, reject) => {
|
|
285
|
+
queue.push({
|
|
286
|
+
topic,
|
|
287
|
+
event,
|
|
288
|
+
resolve,
|
|
289
|
+
reject
|
|
290
|
+
});
|
|
291
|
+
touchIdle();
|
|
292
|
+
ensureOpen();
|
|
293
|
+
});
|
|
294
|
+
},
|
|
295
|
+
onReconnect: hub.onReconnect,
|
|
296
|
+
onStatusChange: hub.onStatusChange,
|
|
297
|
+
onError: hub.onError,
|
|
298
|
+
getStatus: hub.getStatus,
|
|
299
|
+
close() {
|
|
300
|
+
teardown();
|
|
301
|
+
rejectQueue(/* @__PURE__ */ new Error("webSocketTransport is closed"));
|
|
302
|
+
setStatus("disconnected");
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
export { webSocketTransport };
|
|
@@ -28,6 +28,8 @@ export interface ResourceDescriptor {
|
|
|
28
28
|
resource: string;
|
|
29
29
|
fields: FieldDescriptor[];
|
|
30
30
|
}
|
|
31
|
+
/** For core's block editor helpers; not exported from the package index. */
|
|
32
|
+
export declare function describeField(source: string, schema: unknown, references: ResourceConfig["references"]): FieldDescriptor;
|
|
31
33
|
/**
|
|
32
34
|
* Turns a resource's registered Zod schema (Zod 3 or Zod 4) into one descriptor per field, in
|
|
33
35
|
* schema key order. Returns `undefined` when there is no schema to read or it
|