@marianmeres/ownsuite 1.0.3 → 2.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/AGENTS.md +206 -21
- package/API.md +410 -18
- package/README.md +86 -2
- package/dist/adapters/mock-auth.d.ts +38 -0
- package/dist/adapters/mock-auth.js +237 -0
- package/dist/adapters/mock.d.ts +10 -3
- package/dist/adapters/mock.js +79 -25
- package/dist/adapters/mod.d.ts +2 -0
- package/dist/adapters/mod.js +2 -0
- package/dist/adapters/stack-account.d.ts +38 -0
- package/dist/adapters/stack-account.js +149 -0
- package/dist/domains/auth.d.ts +83 -0
- package/dist/domains/auth.js +211 -0
- package/dist/domains/base.d.ts +66 -10
- package/dist/domains/base.js +165 -13
- package/dist/domains/mod.d.ts +3 -0
- package/dist/domains/mod.js +3 -0
- package/dist/domains/owned-collection.d.ts +29 -4
- package/dist/domains/owned-collection.js +240 -120
- package/dist/domains/profile.d.ts +62 -0
- package/dist/domains/profile.js +170 -0
- package/dist/domains/session.d.ts +73 -0
- package/dist/domains/session.js +226 -0
- package/dist/mod.d.ts +1 -0
- package/dist/mod.js +1 -0
- package/dist/oauth/popup.d.ts +64 -0
- package/dist/oauth/popup.js +104 -0
- package/dist/ownsuite.d.ts +58 -5
- package/dist/ownsuite.js +178 -10
- package/dist/types/adapter.d.ts +4 -0
- package/dist/types/auth.d.ts +162 -0
- package/dist/types/auth.js +17 -0
- package/dist/types/events.d.ts +41 -2
- package/dist/types/mod.d.ts +1 -0
- package/dist/types/mod.js +1 -0
- package/dist/types/state.d.ts +17 -0
- package/docs/future-improvements.md +81 -0
- package/package.json +15 -6
|
@@ -5,6 +5,13 @@
|
|
|
5
5
|
* any row shape `TRow` via an injected `OwnedCollectionAdapter`. All CRUD
|
|
6
6
|
* operations are implicitly scoped to the authenticated subject by the
|
|
7
7
|
* server — the client never sets `owner_id`.
|
|
8
|
+
*
|
|
9
|
+
* Concurrency model:
|
|
10
|
+
* - Mutations (create/update/delete) serialize through a per-manager chain.
|
|
11
|
+
* - Reads (initialize/refresh) use abort-supersede: a newer read aborts
|
|
12
|
+
* any in-flight older read.
|
|
13
|
+
* - `onSuccess` callbacks read the live store (not a captured snapshot) so
|
|
14
|
+
* interleaving reads do not erase each other's writes.
|
|
8
15
|
*/
|
|
9
16
|
import type { OwnedCollectionAdapter } from "../types/adapter.js";
|
|
10
17
|
import type { OwnedCollectionState } from "../types/state.js";
|
|
@@ -28,13 +35,31 @@ export declare class OwnedCollectionManager<TRow = Record<string, unknown>, TCre
|
|
|
28
35
|
initialize(): Promise<void>;
|
|
29
36
|
/** Refresh the list from server. Same as initialize but re-entrant. */
|
|
30
37
|
refresh(query?: Record<string, unknown>): Promise<void>;
|
|
31
|
-
/**
|
|
38
|
+
/**
|
|
39
|
+
* Fetch a single row by id. Does NOT update the list and does NOT
|
|
40
|
+
* transition the domain to `error` on failure — a 404 for an un-owned
|
|
41
|
+
* row or a network blip on a read shouldn't invalidate a healthy list
|
|
42
|
+
* view. Emits `own:row:fetched` on success.
|
|
43
|
+
*
|
|
44
|
+
* Returns `null` on any failure (including missing adapter). Callers
|
|
45
|
+
* that need error detail should wrap this method and inspect the thrown
|
|
46
|
+
* adapter error themselves.
|
|
47
|
+
*/
|
|
32
48
|
getOne(id: string): Promise<TRow | null>;
|
|
33
|
-
/** Create a new row.
|
|
49
|
+
/** Create a new row. Server assigns the id; on success, prepends to the list. */
|
|
34
50
|
create(data: TCreate): Promise<TRow | null>;
|
|
35
|
-
/**
|
|
51
|
+
/**
|
|
52
|
+
* Update a row. Optimistically merges `data` into the existing row; on
|
|
53
|
+
* server failure reverts that single row to its pre-call value (without
|
|
54
|
+
* clobbering any interleaved refresh or other mutations).
|
|
55
|
+
*
|
|
56
|
+
* If `id` is not in the current list (e.g., filtered out by an active
|
|
57
|
+
* query or not owned), the optimistic step is a no-op AND the successful
|
|
58
|
+
* server response is NOT inserted — call `refresh()` if you want the
|
|
59
|
+
* row to appear. Emits `own:row:updated` on success regardless.
|
|
60
|
+
*/
|
|
36
61
|
update(id: string, data: TUpdate): Promise<TRow | null>;
|
|
37
|
-
/** Delete a row. Optimistically removes from the list. */
|
|
62
|
+
/** Delete a row. Optimistically removes from the list; re-inserts on failure. */
|
|
38
63
|
delete(id: string): Promise<boolean>;
|
|
39
64
|
/** Snapshot of current rows (empty array if not loaded). */
|
|
40
65
|
getRows(): TRow[];
|
|
@@ -5,13 +5,20 @@
|
|
|
5
5
|
* any row shape `TRow` via an injected `OwnedCollectionAdapter`. All CRUD
|
|
6
6
|
* operations are implicitly scoped to the authenticated subject by the
|
|
7
7
|
* server — the client never sets `owner_id`.
|
|
8
|
+
*
|
|
9
|
+
* Concurrency model:
|
|
10
|
+
* - Mutations (create/update/delete) serialize through a per-manager chain.
|
|
11
|
+
* - Reads (initialize/refresh) use abort-supersede: a newer read aborts
|
|
12
|
+
* any in-flight older read.
|
|
13
|
+
* - `onSuccess` callbacks read the live store (not a captured snapshot) so
|
|
14
|
+
* interleaving reads do not erase each other's writes.
|
|
8
15
|
*/
|
|
9
16
|
import { BaseDomainManager } from "./base.js";
|
|
10
17
|
const defaultGetRowId = (row) => {
|
|
11
18
|
const r = row;
|
|
12
19
|
const id = r.model_id ?? r.id;
|
|
13
|
-
if (typeof id !== "string") {
|
|
14
|
-
throw new Error("OwnedCollectionManager: row has no string `model_id` or `id`; " +
|
|
20
|
+
if (typeof id !== "string" || id === "") {
|
|
21
|
+
throw new Error("OwnedCollectionManager: row has no non-empty string `model_id` or `id`; " +
|
|
15
22
|
"pass a custom `getRowId` in options.");
|
|
16
23
|
}
|
|
17
24
|
return id;
|
|
@@ -31,64 +38,98 @@ export class OwnedCollectionManager extends BaseDomainManager {
|
|
|
31
38
|
if (options.adapter)
|
|
32
39
|
this.adapter = options.adapter;
|
|
33
40
|
}
|
|
41
|
+
/** Build the per-op adapter context with the injected signal. */
|
|
42
|
+
#ctx(signal) {
|
|
43
|
+
return { ...this.context, signal };
|
|
44
|
+
}
|
|
45
|
+
/** Current data or an empty shell. */
|
|
46
|
+
#live() {
|
|
47
|
+
return this.store.get().data ?? { rows: [], meta: {} };
|
|
48
|
+
}
|
|
34
49
|
/** Initialize by fetching the list from the server. */
|
|
35
50
|
async initialize() {
|
|
51
|
+
if (this.isDestroyed)
|
|
52
|
+
return;
|
|
36
53
|
if (!this.adapter) {
|
|
37
54
|
this.setState("ready");
|
|
38
55
|
return;
|
|
39
56
|
}
|
|
40
|
-
this.
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
57
|
+
await this.serializeRead(async (signal) => {
|
|
58
|
+
this.setState("syncing");
|
|
59
|
+
try {
|
|
60
|
+
const res = await this.adapter.list(this.#ctx(signal));
|
|
61
|
+
if (signal.aborted)
|
|
62
|
+
return;
|
|
63
|
+
this.setData({ rows: res.data, meta: res.meta });
|
|
64
|
+
this.markSynced();
|
|
65
|
+
this.emit({
|
|
66
|
+
type: "own:list:fetched",
|
|
67
|
+
domain: this.domainName,
|
|
68
|
+
timestamp: Date.now(),
|
|
69
|
+
count: res.data.length,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
catch (e) {
|
|
73
|
+
if (signal.aborted)
|
|
74
|
+
return;
|
|
75
|
+
this.setError({
|
|
76
|
+
code: "FETCH_FAILED",
|
|
77
|
+
message: e instanceof Error ? e.message : "Failed to fetch list",
|
|
78
|
+
originalError: e,
|
|
79
|
+
operation: "initialize",
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
});
|
|
60
83
|
}
|
|
61
84
|
/** Refresh the list from server. Same as initialize but re-entrant. */
|
|
62
85
|
async refresh(query) {
|
|
63
|
-
if (!this.adapter)
|
|
86
|
+
if (this.isDestroyed || !this.adapter)
|
|
64
87
|
return;
|
|
65
|
-
this.
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
88
|
+
await this.serializeRead(async (signal) => {
|
|
89
|
+
this.setState("syncing");
|
|
90
|
+
try {
|
|
91
|
+
const res = await this.adapter.list(this.#ctx(signal), query);
|
|
92
|
+
if (signal.aborted)
|
|
93
|
+
return;
|
|
94
|
+
this.setData({ rows: res.data, meta: res.meta });
|
|
95
|
+
this.markSynced();
|
|
96
|
+
this.emit({
|
|
97
|
+
type: "own:list:fetched",
|
|
98
|
+
domain: this.domainName,
|
|
99
|
+
timestamp: Date.now(),
|
|
100
|
+
count: res.data.length,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
catch (e) {
|
|
104
|
+
if (signal.aborted)
|
|
105
|
+
return;
|
|
106
|
+
this.setError({
|
|
107
|
+
code: "FETCH_FAILED",
|
|
108
|
+
message: e instanceof Error ? e.message : "Failed to refresh list",
|
|
109
|
+
originalError: e,
|
|
110
|
+
operation: "refresh",
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
});
|
|
85
114
|
}
|
|
86
|
-
/**
|
|
115
|
+
/**
|
|
116
|
+
* Fetch a single row by id. Does NOT update the list and does NOT
|
|
117
|
+
* transition the domain to `error` on failure — a 404 for an un-owned
|
|
118
|
+
* row or a network blip on a read shouldn't invalidate a healthy list
|
|
119
|
+
* view. Emits `own:row:fetched` on success.
|
|
120
|
+
*
|
|
121
|
+
* Returns `null` on any failure (including missing adapter). Callers
|
|
122
|
+
* that need error detail should wrap this method and inspect the thrown
|
|
123
|
+
* adapter error themselves.
|
|
124
|
+
*/
|
|
87
125
|
async getOne(id) {
|
|
88
|
-
if (!this.adapter)
|
|
126
|
+
if (this.isDestroyed || !this.adapter)
|
|
89
127
|
return null;
|
|
128
|
+
const ctrl = this.newController();
|
|
90
129
|
try {
|
|
91
|
-
const res = await this.adapter.getOne(id, this.
|
|
130
|
+
const res = await this.adapter.getOne(id, this.#ctx(ctrl.signal));
|
|
131
|
+
if (ctrl.signal.aborted)
|
|
132
|
+
return null;
|
|
92
133
|
this.emit({
|
|
93
134
|
type: "own:row:fetched",
|
|
94
135
|
domain: this.domainName,
|
|
@@ -98,97 +139,176 @@ export class OwnedCollectionManager extends BaseDomainManager {
|
|
|
98
139
|
return res.data;
|
|
99
140
|
}
|
|
100
141
|
catch (e) {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
142
|
+
if (ctrl.signal.aborted)
|
|
143
|
+
return null;
|
|
144
|
+
this.clog.debug("getOne failed", {
|
|
145
|
+
id,
|
|
146
|
+
error: e instanceof Error ? e.message : e,
|
|
106
147
|
});
|
|
107
148
|
return null;
|
|
108
149
|
}
|
|
150
|
+
finally {
|
|
151
|
+
this.releaseController(ctrl);
|
|
152
|
+
}
|
|
109
153
|
}
|
|
110
|
-
/** Create a new row.
|
|
154
|
+
/** Create a new row. Server assigns the id; on success, prepends to the list. */
|
|
111
155
|
async create(data) {
|
|
112
|
-
if (!this.adapter)
|
|
156
|
+
if (this.isDestroyed || !this.adapter)
|
|
113
157
|
return null;
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
158
|
+
return this.serializeMutation(async () => {
|
|
159
|
+
let result = null;
|
|
160
|
+
await this.withOptimisticUpdate("create", () => {
|
|
161
|
+
// No optimistic insertion — no client-assigned id. `syncing`
|
|
162
|
+
// transition still happens so subscribers see the pending state.
|
|
163
|
+
}, async () => {
|
|
164
|
+
const ctrl = this.newController();
|
|
165
|
+
try {
|
|
166
|
+
const res = await this.adapter.create(data, this.#ctx(ctrl.signal));
|
|
167
|
+
return res.data;
|
|
168
|
+
}
|
|
169
|
+
finally {
|
|
170
|
+
this.releaseController(ctrl);
|
|
171
|
+
}
|
|
172
|
+
}, (serverRow) => {
|
|
173
|
+
result = serverRow;
|
|
174
|
+
// Read live state (not snapshot) so a concurrent delete/refresh is preserved.
|
|
175
|
+
const live = this.#live();
|
|
176
|
+
this.setData({
|
|
177
|
+
rows: [serverRow, ...live.rows],
|
|
178
|
+
meta: live.meta,
|
|
179
|
+
});
|
|
180
|
+
this.emit({
|
|
181
|
+
type: "own:row:created",
|
|
182
|
+
domain: this.domainName,
|
|
183
|
+
timestamp: Date.now(),
|
|
184
|
+
rowId: this.#getRowId(serverRow),
|
|
185
|
+
});
|
|
186
|
+
},
|
|
187
|
+
// No onError: create has no optimistic state to revert; default
|
|
188
|
+
// rollback (restore snapshot) also not needed since we never mutated.
|
|
189
|
+
() => { });
|
|
190
|
+
return result;
|
|
134
191
|
});
|
|
135
|
-
return result;
|
|
136
192
|
}
|
|
137
|
-
/**
|
|
193
|
+
/**
|
|
194
|
+
* Update a row. Optimistically merges `data` into the existing row; on
|
|
195
|
+
* server failure reverts that single row to its pre-call value (without
|
|
196
|
+
* clobbering any interleaved refresh or other mutations).
|
|
197
|
+
*
|
|
198
|
+
* If `id` is not in the current list (e.g., filtered out by an active
|
|
199
|
+
* query or not owned), the optimistic step is a no-op AND the successful
|
|
200
|
+
* server response is NOT inserted — call `refresh()` if you want the
|
|
201
|
+
* row to appear. Emits `own:row:updated` on success regardless.
|
|
202
|
+
*/
|
|
138
203
|
async update(id, data) {
|
|
139
|
-
if (!this.adapter)
|
|
204
|
+
if (this.isDestroyed || !this.adapter)
|
|
140
205
|
return null;
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
206
|
+
return this.serializeMutation(async () => {
|
|
207
|
+
const startData = this.#live();
|
|
208
|
+
const startIdx = startData.rows.findIndex((r) => this.#getRowId(r) === id);
|
|
209
|
+
const originalRow = startIdx !== -1 ? startData.rows[startIdx] : null;
|
|
210
|
+
let result = null;
|
|
211
|
+
await this.withOptimisticUpdate("update", () => {
|
|
212
|
+
if (originalRow === null)
|
|
213
|
+
return;
|
|
214
|
+
const optimistic = {
|
|
215
|
+
...originalRow,
|
|
216
|
+
...data,
|
|
217
|
+
};
|
|
218
|
+
const live = this.#live();
|
|
219
|
+
const liveIdx = live.rows.findIndex((r) => this.#getRowId(r) === id);
|
|
220
|
+
if (liveIdx === -1)
|
|
221
|
+
return;
|
|
222
|
+
const rows = live.rows.slice();
|
|
223
|
+
rows[liveIdx] = optimistic;
|
|
224
|
+
this.setData({ rows, meta: live.meta }, false);
|
|
225
|
+
}, async () => {
|
|
226
|
+
const ctrl = this.newController();
|
|
227
|
+
try {
|
|
228
|
+
const res = await this.adapter.update(id, data, this.#ctx(ctrl.signal));
|
|
229
|
+
return res.data;
|
|
230
|
+
}
|
|
231
|
+
finally {
|
|
232
|
+
this.releaseController(ctrl);
|
|
233
|
+
}
|
|
234
|
+
}, (serverRow) => {
|
|
235
|
+
result = serverRow;
|
|
236
|
+
const live = this.#live();
|
|
237
|
+
const liveIdx = live.rows.findIndex((r) => this.#getRowId(r) === id);
|
|
238
|
+
if (liveIdx !== -1) {
|
|
239
|
+
const rows = live.rows.map((r, i) => (i === liveIdx ? serverRow : r));
|
|
240
|
+
this.setData({ rows, meta: live.meta });
|
|
241
|
+
}
|
|
242
|
+
// If liveIdx === -1 the row is not in the current list; do NOT
|
|
243
|
+
// insert (avoids phantom rows when filtered/unknown).
|
|
244
|
+
this.emit({
|
|
245
|
+
type: "own:row:updated",
|
|
246
|
+
domain: this.domainName,
|
|
247
|
+
timestamp: Date.now(),
|
|
248
|
+
rowId: id,
|
|
249
|
+
});
|
|
250
|
+
}, (_error, _snapshot) => {
|
|
251
|
+
// Per-row rollback: restore the one row we mutated (or nothing).
|
|
252
|
+
if (originalRow === null)
|
|
253
|
+
return;
|
|
254
|
+
const live = this.#live();
|
|
255
|
+
const liveIdx = live.rows.findIndex((r) => this.#getRowId(r) === id);
|
|
256
|
+
if (liveIdx === -1)
|
|
257
|
+
return; // row was removed by another op; don't re-add
|
|
258
|
+
const rows = live.rows.slice();
|
|
259
|
+
rows[liveIdx] = originalRow;
|
|
260
|
+
this.setData({ rows, meta: live.meta }, false);
|
|
165
261
|
});
|
|
262
|
+
return result;
|
|
166
263
|
});
|
|
167
|
-
return result;
|
|
168
264
|
}
|
|
169
|
-
/** Delete a row. Optimistically removes from the list. */
|
|
265
|
+
/** Delete a row. Optimistically removes from the list; re-inserts on failure. */
|
|
170
266
|
async delete(id) {
|
|
171
|
-
if (!this.adapter)
|
|
267
|
+
if (this.isDestroyed || !this.adapter)
|
|
172
268
|
return false;
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
269
|
+
return this.serializeMutation(async () => {
|
|
270
|
+
const startData = this.#live();
|
|
271
|
+
const startIdx = startData.rows.findIndex((r) => this.#getRowId(r) === id);
|
|
272
|
+
const originalRow = startIdx !== -1 ? startData.rows[startIdx] : null;
|
|
273
|
+
let ok = false;
|
|
274
|
+
await this.withOptimisticUpdate("delete", () => {
|
|
275
|
+
const live = this.#live();
|
|
276
|
+
this.setData({
|
|
277
|
+
rows: live.rows.filter((r) => this.#getRowId(r) !== id),
|
|
278
|
+
meta: live.meta,
|
|
279
|
+
}, false);
|
|
280
|
+
}, async () => {
|
|
281
|
+
const ctrl = this.newController();
|
|
282
|
+
try {
|
|
283
|
+
return await this.adapter.delete(id, this.#ctx(ctrl.signal));
|
|
284
|
+
}
|
|
285
|
+
finally {
|
|
286
|
+
this.releaseController(ctrl);
|
|
287
|
+
}
|
|
288
|
+
}, (serverOk) => {
|
|
289
|
+
ok = serverOk;
|
|
290
|
+
this.emit({
|
|
291
|
+
type: "own:row:deleted",
|
|
292
|
+
domain: this.domainName,
|
|
293
|
+
timestamp: Date.now(),
|
|
294
|
+
rowId: id,
|
|
295
|
+
});
|
|
296
|
+
}, (_error, _snapshot) => {
|
|
297
|
+
// Per-row rollback: re-insert the deleted row at its original
|
|
298
|
+
// position, unless it was independently re-added by another op.
|
|
299
|
+
if (originalRow === null)
|
|
300
|
+
return;
|
|
301
|
+
const live = this.#live();
|
|
302
|
+
if (live.rows.some((r) => this.#getRowId(r) === id))
|
|
303
|
+
return;
|
|
304
|
+
const rows = live.rows.slice();
|
|
305
|
+
// Clamp insertion index to current list length.
|
|
306
|
+
const insertAt = Math.min(startIdx, rows.length);
|
|
307
|
+
rows.splice(insertAt < 0 ? 0 : insertAt, 0, originalRow);
|
|
308
|
+
this.setData({ rows, meta: live.meta }, false);
|
|
189
309
|
});
|
|
310
|
+
return ok;
|
|
190
311
|
});
|
|
191
|
-
return ok;
|
|
192
312
|
}
|
|
193
313
|
/** Snapshot of current rows (empty array if not loaded). */
|
|
194
314
|
getRows() {
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module domains/profile
|
|
3
|
+
*
|
|
4
|
+
* ProfileManager — a singleton (one-row) reactive container for the
|
|
5
|
+
* authenticated subject's `/me` data: email, roles, verification flag,
|
|
6
|
+
* whether the account has a password, and the list of linked OAuth
|
|
7
|
+
* connections.
|
|
8
|
+
*
|
|
9
|
+
* Deliberately NOT an OwnedCollectionManager. The underlying `/me` endpoint
|
|
10
|
+
* is a single-record resource: no list, no optimistic create/delete, and
|
|
11
|
+
* update returns the full profile. Shoehorning it into the collection
|
|
12
|
+
* manager would leak CRUD semantics that don't apply and complicate
|
|
13
|
+
* ownership semantics.
|
|
14
|
+
*
|
|
15
|
+
* This manager mutates the companion `SessionManager`'s subject in place
|
|
16
|
+
* when the profile changes (e.g. email edited) so consumers reading from
|
|
17
|
+
* `suite.session` see the update without a second fetch.
|
|
18
|
+
*/
|
|
19
|
+
import { type StoreLike } from "@marianmeres/store";
|
|
20
|
+
import { type PubSub } from "@marianmeres/pubsub";
|
|
21
|
+
import type { OAuthConnection, OAuthProvider, OwnsuiteContext, ProfileAdapter, ProfileResult } from "../types/mod.js";
|
|
22
|
+
import type { SessionManager } from "./session.js";
|
|
23
|
+
export interface ProfileManagerOptions {
|
|
24
|
+
adapter: ProfileAdapter;
|
|
25
|
+
session: SessionManager;
|
|
26
|
+
/** Shared pubsub for event emission. Private if omitted. */
|
|
27
|
+
pubsub?: PubSub;
|
|
28
|
+
/** Initial context passed to adapter calls. Extended per-call with
|
|
29
|
+
* `jwt` and `signal` by the manager. */
|
|
30
|
+
context?: OwnsuiteContext;
|
|
31
|
+
}
|
|
32
|
+
export interface ProfileState {
|
|
33
|
+
/** Null until the first successful fetch. */
|
|
34
|
+
profile: ProfileResult | null;
|
|
35
|
+
/** True while a request is in flight. */
|
|
36
|
+
loading: boolean;
|
|
37
|
+
/** Most recent error, or null. Cleared on the next successful call. */
|
|
38
|
+
error: Error | null;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Profile manager — singleton state for `/me`.
|
|
42
|
+
*/
|
|
43
|
+
export declare class ProfileManager {
|
|
44
|
+
#private;
|
|
45
|
+
constructor(options: ProfileManagerOptions);
|
|
46
|
+
get subscribe(): StoreLike<ProfileState>["subscribe"];
|
|
47
|
+
get(): ProfileState;
|
|
48
|
+
setContext(ctx: OwnsuiteContext): void;
|
|
49
|
+
replaceContext(ctx: OwnsuiteContext): void;
|
|
50
|
+
/** Fetch `/me`. Supersedes any in-flight fetch. */
|
|
51
|
+
fetch(): Promise<ProfileResult>;
|
|
52
|
+
/** Update profile (currently: email). Triggers a re-verification email
|
|
53
|
+
* server-side when the gate is on. Returns the refreshed profile. */
|
|
54
|
+
update(input: {
|
|
55
|
+
email?: string;
|
|
56
|
+
current_password?: string;
|
|
57
|
+
}): Promise<ProfileResult>;
|
|
58
|
+
listOAuth(): Promise<OAuthConnection[]>;
|
|
59
|
+
unlinkOAuth(provider: OAuthProvider): Promise<void>;
|
|
60
|
+
reset(): void;
|
|
61
|
+
destroy(): void;
|
|
62
|
+
}
|