@drift-beacon/plugin 0.1.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/LICENSE +21 -0
- package/README.md +368 -0
- package/bin/dbplugin.js +11 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +38 -0
- package/dist/dev.d.ts +38 -0
- package/dist/dev.js +8 -0
- package/dist/errors.d.ts +7 -0
- package/dist/errors.js +9 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +8 -0
- package/dist/internal/actions.d.ts +35 -0
- package/dist/internal/actions.js +1 -0
- package/dist/internal/collection.d.ts +15 -0
- package/dist/internal/collection.js +52 -0
- package/dist/internal/compatibility.d.ts +26 -0
- package/dist/internal/compatibility.js +52 -0
- package/dist/internal/json-equal.d.ts +3 -0
- package/dist/internal/json-equal.js +20 -0
- package/dist/internal/manifest.d.ts +51 -0
- package/dist/internal/manifest.js +73 -0
- package/dist/internal/models.d.ts +62 -0
- package/dist/internal/models.js +387 -0
- package/dist/internal/package-layout.d.ts +21 -0
- package/dist/internal/package-layout.js +19 -0
- package/dist/internal/protocol.d.ts +96 -0
- package/dist/internal/protocol.js +8 -0
- package/dist/internal/rows.d.ts +41 -0
- package/dist/internal/rows.js +1 -0
- package/dist/internal.d.ts +17 -0
- package/dist/internal.js +13 -0
- package/dist/main.d.ts +92 -0
- package/dist/main.js +14 -0
- package/dist/pack.d.ts +24 -0
- package/dist/pack.js +131 -0
- package/dist/prepare.d.ts +28 -0
- package/dist/prepare.js +121 -0
- package/dist/types.d.ts +193 -0
- package/dist/types.js +10 -0
- package/dist/ui.d.ts +25 -0
- package/dist/ui.js +265 -0
- package/dist/version.d.ts +6 -0
- package/dist/version.js +5 -0
- package/dist/vite.d.ts +8 -0
- package/dist/vite.js +370 -0
- package/package.json +73 -0
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Data and APIs shared by plugin main code (server) and UIs (iframe).
|
|
3
|
+
*
|
|
4
|
+
* Workspace data comes as models: an `Activity`, `Category` or `Session` with its fields, links,
|
|
5
|
+
* derived values and actions. Models read the latest snapshot the app sent, synchronously; there is
|
|
6
|
+
* one model per row for the life of a `ctx`, and `model.data` is its plain, frozen row. Later API
|
|
7
|
+
* versions may add fields and new values to string unions such as `trackingType`, so plugins must
|
|
8
|
+
* ignore what they don't recognise.
|
|
9
|
+
*/
|
|
10
|
+
/** Stops a subscription. Subscriptions made through `ctx` also stop when the instance stops. */
|
|
11
|
+
export type Unsubscribe = () => void;
|
|
12
|
+
/** An activity's plain row: what `activity.data` returns. */
|
|
13
|
+
export interface ActivityData {
|
|
14
|
+
readonly id: string;
|
|
15
|
+
readonly name: string;
|
|
16
|
+
readonly description: string | null;
|
|
17
|
+
readonly trackingType: "span" | "point";
|
|
18
|
+
readonly icon: string;
|
|
19
|
+
/** SVG path data for `icon` (viewBox 0 0 24 24; draw with fill), computed by the app. Null when `icon` isn't a known MDI icon. */
|
|
20
|
+
readonly iconPath: string | null;
|
|
21
|
+
/** Display colour, already resolved from the category when the activity uses its category's colour. */
|
|
22
|
+
readonly color: string;
|
|
23
|
+
readonly categoryId: string | null;
|
|
24
|
+
readonly archived: boolean;
|
|
25
|
+
/** Position within the activity's category. */
|
|
26
|
+
readonly sortOrder: number;
|
|
27
|
+
readonly unit: string | null;
|
|
28
|
+
}
|
|
29
|
+
/** A category's plain row: what `category.data` returns. */
|
|
30
|
+
export interface CategoryData {
|
|
31
|
+
readonly id: string;
|
|
32
|
+
readonly name: string;
|
|
33
|
+
readonly description: string | null;
|
|
34
|
+
readonly color: string;
|
|
35
|
+
readonly icon: string;
|
|
36
|
+
/** SVG path data for `icon` (viewBox 0 0 24 24; draw with fill), computed by the app. Null when `icon` isn't a known MDI icon. */
|
|
37
|
+
readonly iconPath: string | null;
|
|
38
|
+
readonly sortOrder: number;
|
|
39
|
+
}
|
|
40
|
+
/** A session's plain row: what `session.data` returns. */
|
|
41
|
+
export interface SessionData {
|
|
42
|
+
readonly id: string;
|
|
43
|
+
readonly activityId: string;
|
|
44
|
+
/** `span` sessions run from start to end; `point` sessions are a single mark. */
|
|
45
|
+
readonly type: "span" | "point";
|
|
46
|
+
readonly status: "live" | "completed";
|
|
47
|
+
/** Users taking part in the session. */
|
|
48
|
+
readonly memberIds: readonly string[];
|
|
49
|
+
/** When a span started, or when a point was marked. Don't mutate it: copy it first. */
|
|
50
|
+
readonly startedAt: Date;
|
|
51
|
+
/** When a span ended; null while live and for points. */
|
|
52
|
+
readonly endedAt: Date | null;
|
|
53
|
+
}
|
|
54
|
+
/** Fields every model has. */
|
|
55
|
+
export interface Model<Data> {
|
|
56
|
+
/** False once the row is gone from the workspace; the fields then keep their last values. */
|
|
57
|
+
readonly exists: boolean;
|
|
58
|
+
/** The row in the latest snapshot, frozen: a new object whenever the row changes. */
|
|
59
|
+
readonly data: Data;
|
|
60
|
+
}
|
|
61
|
+
export interface Activity extends Readonly<ActivityData>, Model<ActivityData> {
|
|
62
|
+
readonly isSpan: boolean;
|
|
63
|
+
readonly isPoint: boolean;
|
|
64
|
+
/** The current user has a live session of this activity. */
|
|
65
|
+
readonly isLive: boolean;
|
|
66
|
+
readonly category: Category | undefined;
|
|
67
|
+
/** This activity's sessions, newest first. */
|
|
68
|
+
sessions(filter?: Omit<SessionFilter, "activityId">): readonly Session[];
|
|
69
|
+
/** Live sessions of this activity; with `mine: true`, only the current user's. */
|
|
70
|
+
live(options?: {
|
|
71
|
+
readonly mine?: boolean;
|
|
72
|
+
}): readonly Session[];
|
|
73
|
+
/** Start a span session for the current user. */
|
|
74
|
+
start(): Promise<Session>;
|
|
75
|
+
/** Mark a point activity for the current user. */
|
|
76
|
+
mark(): Promise<Session>;
|
|
77
|
+
/** `start()` for a span activity, `mark()` for a point activity. */
|
|
78
|
+
track(): Promise<Session>;
|
|
79
|
+
/** End the current user's live session of this activity. Rejects with `not-found` when there is none. */
|
|
80
|
+
end(): Promise<Session>;
|
|
81
|
+
}
|
|
82
|
+
export interface Category extends Readonly<CategoryData>, Model<CategoryData> {
|
|
83
|
+
/** Activities in this category, sorted. Archived ones are hidden unless `includeArchived` is set. */
|
|
84
|
+
activities(filter?: Omit<ActivityFilter, "categoryId">): readonly Activity[];
|
|
85
|
+
}
|
|
86
|
+
export interface Session extends Readonly<SessionData>, Model<SessionData> {
|
|
87
|
+
readonly isSpan: boolean;
|
|
88
|
+
readonly isPoint: boolean;
|
|
89
|
+
readonly isLive: boolean;
|
|
90
|
+
/** The current user takes part in the session. */
|
|
91
|
+
readonly isMine: boolean;
|
|
92
|
+
readonly activity: Activity | undefined;
|
|
93
|
+
/** Milliseconds from start to end; while live, from start to `at` (default: now). 0 for points. */
|
|
94
|
+
duration(at?: Date): number;
|
|
95
|
+
/** End this live span session. */
|
|
96
|
+
end(): Promise<Session>;
|
|
97
|
+
/** Discard this live span session, as if it never happened. */
|
|
98
|
+
discard(): Promise<void>;
|
|
99
|
+
}
|
|
100
|
+
export interface ActivityFilter {
|
|
101
|
+
/** Activities in this category; `null` for activities without one. */
|
|
102
|
+
readonly categoryId?: string | null;
|
|
103
|
+
readonly trackingType?: "span" | "point";
|
|
104
|
+
/** Archived activities are hidden unless this is true. */
|
|
105
|
+
readonly includeArchived?: boolean;
|
|
106
|
+
}
|
|
107
|
+
export interface SessionFilter {
|
|
108
|
+
readonly activityId?: string;
|
|
109
|
+
readonly status?: "live" | "completed";
|
|
110
|
+
/** Only sessions the current user takes part in. */
|
|
111
|
+
readonly mine?: boolean;
|
|
112
|
+
}
|
|
113
|
+
/** One change to a collection, reported by `onChange`. `previous` is the row before the change. */
|
|
114
|
+
export type Change<T extends Model<unknown>> = {
|
|
115
|
+
readonly type: "added";
|
|
116
|
+
readonly item: T;
|
|
117
|
+
} | {
|
|
118
|
+
readonly type: "updated";
|
|
119
|
+
readonly item: T;
|
|
120
|
+
readonly previous: T["data"];
|
|
121
|
+
} | {
|
|
122
|
+
readonly type: "removed";
|
|
123
|
+
readonly item: T;
|
|
124
|
+
};
|
|
125
|
+
export interface PluginInfo {
|
|
126
|
+
/** Installed identity, for example `github.123.magic-cube`. Differs from the manifest id for installed plugins. */
|
|
127
|
+
readonly id: string;
|
|
128
|
+
readonly version: string;
|
|
129
|
+
/** Base path of this plugin's HTTP routes, for example `/api/plugins/<id>/api`. */
|
|
130
|
+
readonly apiPath: string;
|
|
131
|
+
}
|
|
132
|
+
export interface UserInfo {
|
|
133
|
+
readonly id: string;
|
|
134
|
+
}
|
|
135
|
+
export interface WorkspaceInfo {
|
|
136
|
+
readonly id: string;
|
|
137
|
+
}
|
|
138
|
+
/** The plugin's settings. `dbplugin prepare` fills this in from manifest.json `configuration` (.drift-beacon/config.d.ts). */
|
|
139
|
+
export interface PluginConfig {
|
|
140
|
+
}
|
|
141
|
+
export interface ActivitiesApi {
|
|
142
|
+
/**
|
|
143
|
+
* In the app's order: by category (uncategorized last), then `sortOrder`, then name. Archived
|
|
144
|
+
* activities are hidden unless `includeArchived` is set. The same array comes back until the next snapshot.
|
|
145
|
+
*/
|
|
146
|
+
list(filter?: ActivityFilter): readonly Activity[];
|
|
147
|
+
/** Any activity in the workspace, archived or not. */
|
|
148
|
+
get(id: string): Activity | undefined;
|
|
149
|
+
/** Data change events. They can arrive in bursts after a sync; don't use them to trigger actions. */
|
|
150
|
+
onChange(callback: (change: Change<Activity>) => void): Unsubscribe;
|
|
151
|
+
}
|
|
152
|
+
export interface CategoriesApi {
|
|
153
|
+
/** Sorted by `sortOrder`, then name. The same array comes back until the next snapshot. */
|
|
154
|
+
list(): readonly Category[];
|
|
155
|
+
get(id: string): Category | undefined;
|
|
156
|
+
/** Data change events. They can arrive in bursts after a sync; don't use them to trigger actions. */
|
|
157
|
+
onChange(callback: (change: Change<Category>) => void): Unsubscribe;
|
|
158
|
+
}
|
|
159
|
+
export interface SessionsApi {
|
|
160
|
+
/** Newest first. The same array comes back until the next snapshot. */
|
|
161
|
+
list(filter?: SessionFilter): readonly Session[];
|
|
162
|
+
get(id: string): Session | undefined;
|
|
163
|
+
/** Live sessions in the workspace: `list({ status: "live", mine })`. */
|
|
164
|
+
live(options?: {
|
|
165
|
+
readonly mine?: boolean;
|
|
166
|
+
}): readonly Session[];
|
|
167
|
+
/**
|
|
168
|
+
* Start a span session for the current user. Actions resolve once the snapshot containing
|
|
169
|
+
* their result has arrived, so lists and models already show it.
|
|
170
|
+
*/
|
|
171
|
+
start(activityId: string): Promise<Session>;
|
|
172
|
+
/** End a live span session. */
|
|
173
|
+
end(sessionId: string): Promise<Session>;
|
|
174
|
+
/** Mark a point activity for the current user. */
|
|
175
|
+
mark(activityId: string): Promise<Session>;
|
|
176
|
+
/** Discard a live span session, as if it never happened. Afterwards it is no longer live (or gone). */
|
|
177
|
+
discard(sessionId: string): Promise<void>;
|
|
178
|
+
/** Data change events. They can arrive in bursts after a sync; don't use them to trigger actions. */
|
|
179
|
+
onChange(callback: (change: Change<Session>) => void): Unsubscribe;
|
|
180
|
+
}
|
|
181
|
+
/** The plugin's own key-value storage, per user and workspace. Values must be JSON-compatible. */
|
|
182
|
+
export interface StorageApi {
|
|
183
|
+
get<T = unknown>(key: string): T | undefined;
|
|
184
|
+
keys(): readonly string[];
|
|
185
|
+
/** Applies locally at once and resolves when the server has stored it. */
|
|
186
|
+
set(key: string, value: unknown): Promise<void>;
|
|
187
|
+
remove(key: string): Promise<void>;
|
|
188
|
+
/** Called for changes made anywhere else: main code, a UI or another device. */
|
|
189
|
+
onChange(callback: (key: string, value: unknown) => void): Unsubscribe;
|
|
190
|
+
}
|
|
191
|
+
/** Error codes on rejected operations (`error.code`). */
|
|
192
|
+
export type PluginErrorCode = "unsupported" | "invalid" | "not-found" | "unavailable" | "stopped" | "failed";
|
|
193
|
+
//# sourceMappingURL=types.d.ts.map
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Data and APIs shared by plugin main code (server) and UIs (iframe).
|
|
3
|
+
*
|
|
4
|
+
* Workspace data comes as models: an `Activity`, `Category` or `Session` with its fields, links,
|
|
5
|
+
* derived values and actions. Models read the latest snapshot the app sent, synchronously; there is
|
|
6
|
+
* one model per row for the life of a `ctx`, and `model.data` is its plain, frozen row. Later API
|
|
7
|
+
* versions may add fields and new values to string unions such as `trackingType`, so plugins must
|
|
8
|
+
* ignore what they don't recognise.
|
|
9
|
+
*/
|
|
10
|
+
export {};
|
package/dist/ui.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ActivitiesApi, CategoriesApi, PluginConfig, PluginInfo, SessionsApi, StorageApi, Unsubscribe, UserInfo, WorkspaceInfo } from "./types.ts";
|
|
2
|
+
export { PluginError } from "./errors.ts";
|
|
3
|
+
export type * from "./types.ts";
|
|
4
|
+
export { API_VERSION } from "./version.ts";
|
|
5
|
+
/** Everything a plugin UI can use. */
|
|
6
|
+
export interface UiContext {
|
|
7
|
+
readonly plugin: PluginInfo;
|
|
8
|
+
readonly user: UserInfo;
|
|
9
|
+
readonly workspace: WorkspaceInfo;
|
|
10
|
+
/** The plugin's current settings; they can change while the UI is open. */
|
|
11
|
+
readonly config: Readonly<PluginConfig>;
|
|
12
|
+
readonly activities: ActivitiesApi;
|
|
13
|
+
readonly categories: CategoriesApi;
|
|
14
|
+
readonly sessions: SessionsApi;
|
|
15
|
+
/** Called after each update from the app (workspace data, config or storage) and after local storage writes. */
|
|
16
|
+
onDataChange(callback: () => void): Unsubscribe;
|
|
17
|
+
readonly storage: StorageApi;
|
|
18
|
+
}
|
|
19
|
+
export interface ConnectOptions {
|
|
20
|
+
/** How long to wait for the app to answer. Default 10 seconds. */
|
|
21
|
+
readonly timeoutMs?: number;
|
|
22
|
+
}
|
|
23
|
+
/** Connect to the Drift Beacon web app. Resolves once the app accepts this UI; later calls return the same context. */
|
|
24
|
+
export declare function connect(options?: ConnectOptions): Promise<UiContext>;
|
|
25
|
+
//# sourceMappingURL=ui.d.ts.map
|
package/dist/ui.js
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The UI client: runs inside a plugin's iframe and talks to the Drift Beacon web app.
|
|
3
|
+
*
|
|
4
|
+
* ```ts
|
|
5
|
+
* const ctx = await connect();
|
|
6
|
+
* ctx.onDataChange(render);
|
|
7
|
+
* ```
|
|
8
|
+
*/
|
|
9
|
+
import { PluginError } from "./errors.js";
|
|
10
|
+
import { Collection } from "./internal/collection.js";
|
|
11
|
+
import { jsonEqual } from "./internal/json-equal.js";
|
|
12
|
+
import { WorkspaceModels } from "./internal/models.js";
|
|
13
|
+
import { isUiMessage, UI_CHANNEL, } from "./internal/protocol.js";
|
|
14
|
+
import { API_VERSION } from "./version.js";
|
|
15
|
+
export { PluginError } from "./errors.js";
|
|
16
|
+
export { API_VERSION } from "./version.js";
|
|
17
|
+
const HELLO_INTERVAL_MS = 250;
|
|
18
|
+
const REQUEST_TIMEOUT_MS = 10_000;
|
|
19
|
+
let connection = null;
|
|
20
|
+
/** Connect to the Drift Beacon web app. Resolves once the app accepts this UI; later calls return the same context. */
|
|
21
|
+
export function connect(options = {}) {
|
|
22
|
+
connection ??= open(options.timeoutMs ?? 10_000);
|
|
23
|
+
return connection;
|
|
24
|
+
}
|
|
25
|
+
function open(timeoutMs) {
|
|
26
|
+
if (typeof window === "undefined" || window.parent === window) {
|
|
27
|
+
return Promise.reject(new PluginError("unavailable", "A plugin UI must run inside Drift Beacon"));
|
|
28
|
+
}
|
|
29
|
+
const host = window.parent;
|
|
30
|
+
const post = (message) => host.postMessage(message, "*");
|
|
31
|
+
return new Promise((resolve, reject) => {
|
|
32
|
+
let client = null;
|
|
33
|
+
const finish = () => {
|
|
34
|
+
clearInterval(hello);
|
|
35
|
+
clearTimeout(timeout);
|
|
36
|
+
};
|
|
37
|
+
const onMessage = (event) => {
|
|
38
|
+
if (event.source !== host || !isUiMessage(event.data))
|
|
39
|
+
return;
|
|
40
|
+
const message = event.data;
|
|
41
|
+
if (message.type === "reconnect") {
|
|
42
|
+
sayHello();
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (client) {
|
|
46
|
+
client.receive(message);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (message.type === "welcome") {
|
|
50
|
+
finish();
|
|
51
|
+
client = new UiClient(message, post);
|
|
52
|
+
resolve(client.context);
|
|
53
|
+
}
|
|
54
|
+
else if (message.type === "reject") {
|
|
55
|
+
finish();
|
|
56
|
+
window.removeEventListener("message", onMessage);
|
|
57
|
+
reject(new PluginError("unsupported", message.reason));
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
window.addEventListener("message", onMessage);
|
|
61
|
+
const sayHello = () => post({ channel: UI_CHANNEL, type: "hello", apiVersion: API_VERSION });
|
|
62
|
+
// Repeat until answered: the app may still be setting up when the UI first loads.
|
|
63
|
+
const hello = setInterval(sayHello, HELLO_INTERVAL_MS);
|
|
64
|
+
sayHello();
|
|
65
|
+
const timeout = setTimeout(() => {
|
|
66
|
+
finish();
|
|
67
|
+
window.removeEventListener("message", onMessage);
|
|
68
|
+
reject(new PluginError("unavailable", "Drift Beacon did not answer. Reload the plugin."));
|
|
69
|
+
}, timeoutMs);
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
class UiClient {
|
|
73
|
+
post;
|
|
74
|
+
context;
|
|
75
|
+
config;
|
|
76
|
+
rows = {
|
|
77
|
+
activities: new Collection(),
|
|
78
|
+
categories: new Collection(),
|
|
79
|
+
sessions: new Collection(),
|
|
80
|
+
};
|
|
81
|
+
models;
|
|
82
|
+
activityListeners = new Set();
|
|
83
|
+
categoryListeners = new Set();
|
|
84
|
+
sessionListeners = new Set();
|
|
85
|
+
dataListeners = new Set();
|
|
86
|
+
storageListeners = new Set();
|
|
87
|
+
storage = new Map();
|
|
88
|
+
serverStorage;
|
|
89
|
+
/** Keys with writes the app hasn't answered; older pushes must not revert them. */
|
|
90
|
+
pendingWrites = new Map();
|
|
91
|
+
/**
|
|
92
|
+
* Keys written since the app last sent storage, with the latest write's ID. The app may answer a
|
|
93
|
+
* write before its next push, so an answered write stays in place until that push arrives.
|
|
94
|
+
*/
|
|
95
|
+
writesSincePush = new Map();
|
|
96
|
+
nextWrite = 0;
|
|
97
|
+
pending = new Map();
|
|
98
|
+
nextRequest = 0;
|
|
99
|
+
constructor(welcome, post) {
|
|
100
|
+
this.post = post;
|
|
101
|
+
this.config = welcome.config;
|
|
102
|
+
this.serverStorage = welcome.storage;
|
|
103
|
+
this.storage = new Map(Object.entries(welcome.storage));
|
|
104
|
+
this.models = new WorkspaceModels({
|
|
105
|
+
userId: welcome.user.id,
|
|
106
|
+
rows: this.rows,
|
|
107
|
+
actions: {
|
|
108
|
+
start: (activityId) => this.request({ method: "sessions.start", params: { activityId } }),
|
|
109
|
+
end: (sessionId) => this.request({ method: "sessions.end", params: { sessionId } }),
|
|
110
|
+
mark: (activityId) => this.request({ method: "sessions.mark", params: { activityId } }),
|
|
111
|
+
discard: (sessionId) => this.request({ method: "sessions.discard", params: { sessionId } }),
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
this.applyData(welcome.data);
|
|
115
|
+
const apis = this.models.apis();
|
|
116
|
+
const self = this;
|
|
117
|
+
const subscribe = (listeners, callback) => {
|
|
118
|
+
listeners.add(callback);
|
|
119
|
+
return () => listeners.delete(callback);
|
|
120
|
+
};
|
|
121
|
+
this.context = {
|
|
122
|
+
plugin: welcome.plugin,
|
|
123
|
+
user: welcome.user,
|
|
124
|
+
workspace: welcome.workspace,
|
|
125
|
+
get config() {
|
|
126
|
+
return self.config;
|
|
127
|
+
},
|
|
128
|
+
activities: { ...apis.activities, onChange: (callback) => subscribe(this.activityListeners, callback) },
|
|
129
|
+
categories: { ...apis.categories, onChange: (callback) => subscribe(this.categoryListeners, callback) },
|
|
130
|
+
sessions: { ...apis.sessions, onChange: (callback) => subscribe(this.sessionListeners, callback) },
|
|
131
|
+
onDataChange: (callback) => subscribe(this.dataListeners, callback),
|
|
132
|
+
storage: {
|
|
133
|
+
get: (key) => this.storage.get(key),
|
|
134
|
+
keys: () => [...this.storage.keys()],
|
|
135
|
+
set: (key, value) => this.write(key, JSON.parse(JSON.stringify(value))),
|
|
136
|
+
remove: (key) => this.write(key, undefined),
|
|
137
|
+
onChange: (callback) => subscribe(this.storageListeners, callback),
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
receive(message) {
|
|
142
|
+
if (message.type === "welcome") {
|
|
143
|
+
// A reconnect: identity is unchanged, so only the state is new.
|
|
144
|
+
this.receive({
|
|
145
|
+
channel: message.channel,
|
|
146
|
+
type: "state",
|
|
147
|
+
config: message.config,
|
|
148
|
+
data: message.data,
|
|
149
|
+
storage: message.storage,
|
|
150
|
+
});
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (message.type === "response") {
|
|
154
|
+
const pending = this.pending.get(message.id);
|
|
155
|
+
if (!pending)
|
|
156
|
+
return;
|
|
157
|
+
this.pending.delete(message.id);
|
|
158
|
+
if (message.error)
|
|
159
|
+
pending.reject(new PluginError(message.error.code, message.error.message));
|
|
160
|
+
else
|
|
161
|
+
pending.resolve(message.result);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (message.type !== "state")
|
|
165
|
+
return;
|
|
166
|
+
if (message.config)
|
|
167
|
+
this.config = message.config;
|
|
168
|
+
if (message.data)
|
|
169
|
+
this.applyData(message.data);
|
|
170
|
+
if (message.storage) {
|
|
171
|
+
this.serverStorage = message.storage;
|
|
172
|
+
this.writesSincePush.clear();
|
|
173
|
+
this.reconcileStorage();
|
|
174
|
+
}
|
|
175
|
+
notify(this.dataListeners, (listener) => listener());
|
|
176
|
+
}
|
|
177
|
+
/** Replace all three collections before telling anyone, so callbacks see one consistent snapshot. */
|
|
178
|
+
applyData(data) {
|
|
179
|
+
const changes = this.models.update({
|
|
180
|
+
activities: this.rows.activities.replace(data.activities),
|
|
181
|
+
categories: this.rows.categories.replace(data.categories),
|
|
182
|
+
sessions: this.rows.sessions.replace(data.sessions),
|
|
183
|
+
});
|
|
184
|
+
for (const change of changes.activities)
|
|
185
|
+
notify(this.activityListeners, (listener) => listener(change));
|
|
186
|
+
for (const change of changes.categories)
|
|
187
|
+
notify(this.categoryListeners, (listener) => listener(change));
|
|
188
|
+
for (const change of changes.sessions)
|
|
189
|
+
notify(this.sessionListeners, (listener) => listener(change));
|
|
190
|
+
}
|
|
191
|
+
/** Apply the app's storage, keeping local values for keys with unconfirmed writes. Returns whether anything changed. */
|
|
192
|
+
reconcileStorage() {
|
|
193
|
+
const isProtected = (key) => this.pendingWrites.has(key) || this.writesSincePush.has(key);
|
|
194
|
+
const next = new Map();
|
|
195
|
+
for (const [key, value] of Object.entries(this.serverStorage))
|
|
196
|
+
if (!isProtected(key))
|
|
197
|
+
next.set(key, value);
|
|
198
|
+
for (const [key, value] of this.storage)
|
|
199
|
+
if (isProtected(key))
|
|
200
|
+
next.set(key, value);
|
|
201
|
+
const previous = this.storage;
|
|
202
|
+
this.storage = next;
|
|
203
|
+
let changed = false;
|
|
204
|
+
for (const key of new Set([...previous.keys(), ...next.keys()])) {
|
|
205
|
+
if (jsonEqual(previous.get(key), next.get(key)))
|
|
206
|
+
continue;
|
|
207
|
+
changed = true;
|
|
208
|
+
if (!isProtected(key))
|
|
209
|
+
notify(this.storageListeners, (listener) => listener(key, next.get(key)));
|
|
210
|
+
}
|
|
211
|
+
return changed;
|
|
212
|
+
}
|
|
213
|
+
async write(key, value) {
|
|
214
|
+
const writeId = ++this.nextWrite;
|
|
215
|
+
this.pendingWrites.set(key, (this.pendingWrites.get(key) ?? 0) + 1);
|
|
216
|
+
this.writesSincePush.set(key, writeId);
|
|
217
|
+
if (value === undefined)
|
|
218
|
+
this.storage.delete(key);
|
|
219
|
+
else
|
|
220
|
+
this.storage.set(key, value);
|
|
221
|
+
notify(this.dataListeners, (listener) => listener());
|
|
222
|
+
try {
|
|
223
|
+
await this.request(value === undefined
|
|
224
|
+
? { method: "storage.remove", params: { key } }
|
|
225
|
+
: { method: "storage.set", params: { key, value } });
|
|
226
|
+
}
|
|
227
|
+
catch (error) {
|
|
228
|
+
// Roll back to the app's value, unless a later local write replaced this one.
|
|
229
|
+
if (this.writesSincePush.get(key) === writeId)
|
|
230
|
+
this.writesSincePush.delete(key);
|
|
231
|
+
throw error;
|
|
232
|
+
}
|
|
233
|
+
finally {
|
|
234
|
+
const remaining = (this.pendingWrites.get(key) ?? 1) - 1;
|
|
235
|
+
if (remaining > 0)
|
|
236
|
+
this.pendingWrites.set(key, remaining);
|
|
237
|
+
else
|
|
238
|
+
this.pendingWrites.delete(key);
|
|
239
|
+
if (this.reconcileStorage())
|
|
240
|
+
notify(this.dataListeners, (listener) => listener());
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
request(request) {
|
|
244
|
+
const id = String(++this.nextRequest);
|
|
245
|
+
return new Promise((resolve, reject) => {
|
|
246
|
+
this.pending.set(id, { resolve, reject });
|
|
247
|
+
this.post({ channel: UI_CHANNEL, type: "request", id, ...request });
|
|
248
|
+
setTimeout(() => {
|
|
249
|
+
if (!this.pending.delete(id))
|
|
250
|
+
return;
|
|
251
|
+
reject(new PluginError("unavailable", `${request.method} timed out`));
|
|
252
|
+
}, REQUEST_TIMEOUT_MS);
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
function notify(listeners, call) {
|
|
257
|
+
for (const listener of listeners) {
|
|
258
|
+
try {
|
|
259
|
+
call(listener);
|
|
260
|
+
}
|
|
261
|
+
catch (error) {
|
|
262
|
+
console.error("[drift-beacon] Error in plugin listener:", error);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
package/dist/version.js
ADDED
package/dist/vite.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Vite plugin for a Drift Beacon plugin package: `plugins: [driftBeacon()]`. It owns `root`, `base` and
|
|
3
|
+
* `build.outDir`. Typed as `{ name: string }` so it fits any Vite version's `plugins`.
|
|
4
|
+
*/
|
|
5
|
+
export declare function driftBeacon(): {
|
|
6
|
+
name: string;
|
|
7
|
+
};
|
|
8
|
+
//# sourceMappingURL=vite.d.ts.map
|