mnfst 0.5.255 → 0.5.257
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/lib/manifest.appwrite.auth.js +311 -1
- package/lib/manifest.appwrite.data.js +331 -220
- package/lib/manifest.appwrite.presences.js +406 -42
- package/lib/manifest.d.ts +1 -1
- package/lib/manifest.data.js +1 -1
- package/lib/manifest.device.js +4 -3
- package/lib/manifest.integrity.json +7 -7
- package/lib/manifest.native.js +4 -3
- package/lib/manifest.schema.json +34 -0
- package/lib/manifest.sw.js +1 -1
- package/package.json +1 -1
|
@@ -6,6 +6,284 @@
|
|
|
6
6
|
/* By Andrew Matlock under MIT license
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
// mnfst:realtime.hub:start
|
|
10
|
+
/* Shared Appwrite Realtime hub — ONE Realtime instance (one socket) per tab
|
|
11
|
+
across the data, auth and presences plugins. The first carrier to load
|
|
12
|
+
defines it; the build syncs this block byte-identical into each carrier
|
|
13
|
+
from scripts/snippets/manifest.realtime.hub.js.
|
|
14
|
+
|
|
15
|
+
Subscriptions are keyed by a caller-chosen name and ref-counted by
|
|
16
|
+
(channel + canonical server-filter key); server-side realtime queries ride
|
|
17
|
+
per subscription where the instance supports them (verified SDK/server
|
|
18
|
+
contract: see the manifest.data.realtime.js header). Messages are
|
|
19
|
+
classified ONCE — Appwrite sends ~20 event-name variants per change —
|
|
20
|
+
so each handler gets one (action, payload, response) call per message. */
|
|
21
|
+
if (!window.ManifestRealtimeHub) {
|
|
22
|
+
window.ManifestRealtimeHub = (function () {
|
|
23
|
+
// Identity events drop the instance memo (the socket re-authenticates
|
|
24
|
+
// as the new session) and re-bind live subscriptions
|
|
25
|
+
const AUTH_EVENTS = ['manifest:auth:login', 'manifest:auth:logout', 'manifest:auth:anonymous', 'manifest:auth:session-cleared', 'manifest:auth:initialized'];
|
|
26
|
+
|
|
27
|
+
// One Realtime (one socket) per tab, keyed to the client it wraps
|
|
28
|
+
let realtimeCache = null;
|
|
29
|
+
let realtimeClient = null;
|
|
30
|
+
function acquire(client) {
|
|
31
|
+
if (!client || !window.Appwrite?.Realtime) return null;
|
|
32
|
+
if (!realtimeCache || realtimeClient !== client) {
|
|
33
|
+
realtimeCache = new window.Appwrite.Realtime(client);
|
|
34
|
+
realtimeClient = client;
|
|
35
|
+
}
|
|
36
|
+
return realtimeCache;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Map<subKey, { channel, queries, refs, unsubscribe, handlers: Map<name, handler> }>
|
|
40
|
+
const entries = new Map();
|
|
41
|
+
// Map<name, { subKey, channel, token, handler, getFilters, getRealtime }>
|
|
42
|
+
const sources = new Map();
|
|
43
|
+
// Map<name, fn(action, payload, response)> — pre-filters, run once per message per name
|
|
44
|
+
const filters = new Map();
|
|
45
|
+
// Map<name, n> — explicit teardowns; aborts a subscribe in flight
|
|
46
|
+
const tombstones = new Map();
|
|
47
|
+
// Instance the current subscriptions live on
|
|
48
|
+
let activeRealtime = null;
|
|
49
|
+
|
|
50
|
+
// Reduce a message's event-name variants to the single most specific
|
|
51
|
+
// action: delete > update (upsert counts: presences) > create
|
|
52
|
+
function classify(events) {
|
|
53
|
+
const list = Array.isArray(events) ? events : (events ? [events] : []);
|
|
54
|
+
let action = null;
|
|
55
|
+
for (const event of list) {
|
|
56
|
+
if (typeof event !== 'string') continue;
|
|
57
|
+
// Verbs match as path segments only — event names embed
|
|
58
|
+
// user-chosen ids (a table named deleted_posts is not a delete)
|
|
59
|
+
const parts = event.split('.');
|
|
60
|
+
if (parts.includes('delete')) return 'delete';
|
|
61
|
+
if (parts.includes('update') || parts.includes('upsert')) action = 'update';
|
|
62
|
+
else if (!action && parts.includes('create')) action = 'create';
|
|
63
|
+
}
|
|
64
|
+
return action;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Query-capable Realtime service: detect per instance, never assume
|
|
68
|
+
function supportsQueries(realtime) {
|
|
69
|
+
return !!(realtime && (realtime.pendingSubscribes || realtime.slotToSubscriptionId));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// (channel, resolved filter set) → stable subscription key; resolved.key
|
|
73
|
+
// is already canonical so equivalent filter sets share one subscription
|
|
74
|
+
function subKeyFor(channel, resolved) {
|
|
75
|
+
if (!resolved || !resolved.key) return channel;
|
|
76
|
+
return `${channel}\u001f${resolved.key}`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Fan one SDK message out, classified once: each registered handler gets
|
|
80
|
+
// ONE (action, payload, response) call per message, after that name's
|
|
81
|
+
// pre-filter (setFilter) — before any merge/dedup work
|
|
82
|
+
function dispatch(subKey, response) {
|
|
83
|
+
const entry = entries.get(subKey);
|
|
84
|
+
if (!entry || !response || !response.events) return;
|
|
85
|
+
const action = classify(response.events);
|
|
86
|
+
if (!action) return;
|
|
87
|
+
let payload = response.payload;
|
|
88
|
+
if (!payload || (!payload.$id && !payload.row)) payload = response;
|
|
89
|
+
for (const [name, handler] of entry.handlers) {
|
|
90
|
+
const filter = filters.get(name);
|
|
91
|
+
if (filter && filter(action, payload, response) === false) continue;
|
|
92
|
+
handler(action, payload, response);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// subscribe() returns an unsubscribe function (legacy client) or a
|
|
97
|
+
// Promise<RealtimeSubscription> (Realtime service): normalize teardown
|
|
98
|
+
function dropSub(sub) {
|
|
99
|
+
if (typeof sub === 'function') sub();
|
|
100
|
+
else if (sub && typeof sub.unsubscribe === 'function') sub.unsubscribe(); // per-subscription; never risks the shared socket
|
|
101
|
+
else if (sub && typeof sub.close === 'function') sub.close();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function makeUnsubscriber(result) {
|
|
105
|
+
if (!result || typeof result.then !== 'function') {
|
|
106
|
+
return () => { try { dropSub(result); } catch (e) { /* socket already gone */ } };
|
|
107
|
+
}
|
|
108
|
+
const settled = result.catch(() => null);
|
|
109
|
+
return () => { settled.then(dropSub).catch(() => { /* socket already gone */ }); };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function sdkSubscribe(realtime, subKey, channel, queries) {
|
|
113
|
+
const cb = response => dispatch(subKey, response);
|
|
114
|
+
const result = (queries && queries.length)
|
|
115
|
+
? realtime.subscribe(channel, cb, queries)
|
|
116
|
+
: realtime.subscribe(channel, cb);
|
|
117
|
+
return makeUnsubscriber(result);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Get or create the shared entry for (channel, filters); registered in the
|
|
121
|
+
// map before the SDK call so an event arriving mid-subscribe already dispatches
|
|
122
|
+
function ensureEntry(realtime, subKey, channel, queries) {
|
|
123
|
+
let entry = entries.get(subKey);
|
|
124
|
+
if (entry) return entry;
|
|
125
|
+
entry = { channel, queries: queries || [], refs: 0, unsubscribe: null, handlers: new Map() };
|
|
126
|
+
entries.set(subKey, entry);
|
|
127
|
+
try {
|
|
128
|
+
entry.unsubscribe = sdkSubscribe(realtime, subKey, channel, queries);
|
|
129
|
+
} catch (error) {
|
|
130
|
+
entries.delete(subKey);
|
|
131
|
+
throw error;
|
|
132
|
+
}
|
|
133
|
+
return entry;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function removeFromEntry(name, subKey) {
|
|
137
|
+
const entry = entries.get(subKey);
|
|
138
|
+
if (!entry) return;
|
|
139
|
+
entry.handlers.delete(name);
|
|
140
|
+
entry.refs = entry.handlers.size;
|
|
141
|
+
if (entry.refs === 0) {
|
|
142
|
+
entries.delete(subKey);
|
|
143
|
+
if (typeof entry.unsubscribe === 'function') {
|
|
144
|
+
try { entry.unsubscribe(); } catch (e) { /* socket already gone */ }
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Drop a name's registration; tear the subscription down when the last one leaves
|
|
150
|
+
function detach(name, token) {
|
|
151
|
+
const cur = sources.get(name);
|
|
152
|
+
if (!cur || (token && cur.token !== token)) return;
|
|
153
|
+
sources.delete(name);
|
|
154
|
+
removeFromEntry(name, cur.subKey);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Instance changed (rebuilt on identity change): re-resolve each name's
|
|
158
|
+
// filters ($auth. values are stale) and regroup subscriptions on the new
|
|
159
|
+
// instance. Chained so overlapping rebuilds run in order; superseded stops.
|
|
160
|
+
let syncChain = Promise.resolve();
|
|
161
|
+
function syncInstance(realtime) {
|
|
162
|
+
if (activeRealtime === realtime) return syncChain;
|
|
163
|
+
activeRealtime = realtime;
|
|
164
|
+
syncChain = syncChain
|
|
165
|
+
.then(() => rebindAll(realtime))
|
|
166
|
+
.catch(error => console.error('[Manifest Realtime] Rebind failed:', error));
|
|
167
|
+
return syncChain;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async function rebindAll(realtime) {
|
|
171
|
+
for (const [, entry] of entries) {
|
|
172
|
+
if (typeof entry.unsubscribe === 'function') {
|
|
173
|
+
try { entry.unsubscribe(); } catch (e) { /* old instance */ }
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
entries.clear();
|
|
177
|
+
const canQuery = supportsQueries(realtime);
|
|
178
|
+
for (const [name, src] of sources) {
|
|
179
|
+
if (activeRealtime !== realtime) return; // superseded by a newer instance
|
|
180
|
+
try {
|
|
181
|
+
// Inside the try: one source's failing getFilters must not
|
|
182
|
+
// strand the sources after it unsubscribed
|
|
183
|
+
const resolved = (canQuery && src.getFilters) ? await src.getFilters() : null;
|
|
184
|
+
if (activeRealtime !== realtime) return;
|
|
185
|
+
const subKey = subKeyFor(src.channel, resolved);
|
|
186
|
+
const entry = ensureEntry(realtime, subKey, src.channel, resolved?.queries);
|
|
187
|
+
entry.handlers.set(name, src.handler);
|
|
188
|
+
entry.refs = entry.handlers.size;
|
|
189
|
+
src.subKey = subKey;
|
|
190
|
+
} catch (error) {
|
|
191
|
+
console.error('[Manifest Realtime] Failed to re-subscribe channel:', src.channel, error);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// One SDK subscribe per distinct (channel, filter set); names sharing both
|
|
197
|
+
// share it. getRealtime: async () => instance (re-run on identity rebuilds).
|
|
198
|
+
// isCurrent: false once the requesting load is superseded — skip, never
|
|
199
|
+
// touch a newer entry. getFilters: async () => ({ queries, key }) | null,
|
|
200
|
+
// re-run per (re)subscribe; null → channel-only subscribe. A replaced
|
|
201
|
+
// registration detaches only after the new subscription is live (no event
|
|
202
|
+
// gap); overlap double-delivery is the consumer's dedupe to absorb.
|
|
203
|
+
async function subscribe({ name, channel, onEvent, isCurrent, getFilters, getRealtime }) {
|
|
204
|
+
const epoch = tombstones.get(name) || 0;
|
|
205
|
+
const realtime = getRealtime ? await getRealtime() : null;
|
|
206
|
+
if (isCurrent && !isCurrent()) return null;
|
|
207
|
+
if (!realtime) {
|
|
208
|
+
detach(name);
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
await syncInstance(realtime);
|
|
212
|
+
if (isCurrent && !isCurrent()) return null;
|
|
213
|
+
|
|
214
|
+
const resolved = (supportsQueries(realtime) && getFilters) ? await getFilters() : null;
|
|
215
|
+
if (isCurrent && !isCurrent()) return null;
|
|
216
|
+
// Torn down while resolving: land nothing
|
|
217
|
+
if ((tombstones.get(name) || 0) !== epoch) return null;
|
|
218
|
+
// Instance replaced while resolving (identity rebuild): re-run against the live one
|
|
219
|
+
if (activeRealtime !== realtime) return subscribe({ name, channel, onEvent, isCurrent, getFilters, getRealtime });
|
|
220
|
+
const subKey = subKeyFor(channel, resolved);
|
|
221
|
+
|
|
222
|
+
const prev = sources.get(name);
|
|
223
|
+
let entry;
|
|
224
|
+
try {
|
|
225
|
+
entry = ensureEntry(realtime, subKey, channel, resolved?.queries);
|
|
226
|
+
} catch (error) {
|
|
227
|
+
console.error('[Manifest Realtime] Failed to subscribe:', channel, error);
|
|
228
|
+
detach(name);
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
const token = Symbol(name);
|
|
232
|
+
entry.handlers.set(name, onEvent);
|
|
233
|
+
entry.refs = entry.handlers.size;
|
|
234
|
+
sources.set(name, { subKey, channel, token, handler: onEvent, getFilters, getRealtime });
|
|
235
|
+
if (prev && prev.subKey !== subKey) removeFromEntry(name, prev.subKey);
|
|
236
|
+
|
|
237
|
+
return () => detach(name, token);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Explicit teardown by name; also aborts a subscribe in flight and
|
|
241
|
+
// drops the name's pre-filter (a replacement re-registers its own)
|
|
242
|
+
function unsubscribe(name) {
|
|
243
|
+
tombstones.set(name, (tombstones.get(name) || 0) + 1);
|
|
244
|
+
filters.delete(name);
|
|
245
|
+
detach(name);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Programmatic pre-filter: fn(action, payload, response) runs once per
|
|
249
|
+
// message per name BEFORE the handler; false drops it. null clears.
|
|
250
|
+
function setFilter(name, fn) {
|
|
251
|
+
if (fn == null) filters.delete(name);
|
|
252
|
+
else if (typeof fn === 'function') filters.set(name, fn);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Identity events: drop the memo, then re-bind live subscriptions on a
|
|
256
|
+
// fresh instance from any registered source's getter
|
|
257
|
+
if (typeof window !== 'undefined') {
|
|
258
|
+
AUTH_EVENTS.forEach(type => window.addEventListener(type, async () => {
|
|
259
|
+
realtimeCache = null;
|
|
260
|
+
realtimeClient = null;
|
|
261
|
+
if (!sources.size) return;
|
|
262
|
+
try {
|
|
263
|
+
const src = [...sources.values()].find(s => s.getRealtime);
|
|
264
|
+
const realtime = src ? await src.getRealtime() : null;
|
|
265
|
+
if (realtime) syncInstance(realtime);
|
|
266
|
+
} catch (error) {
|
|
267
|
+
console.error('[Manifest Realtime] Identity rebuild failed:', error);
|
|
268
|
+
}
|
|
269
|
+
}));
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return {
|
|
273
|
+
AUTH_EVENTS,
|
|
274
|
+
acquire,
|
|
275
|
+
subscribe,
|
|
276
|
+
unsubscribe,
|
|
277
|
+
setFilter,
|
|
278
|
+
classify,
|
|
279
|
+
supportsQueries,
|
|
280
|
+
_entries: entries, // Expose for debugging (keyed by channel + filter set)
|
|
281
|
+
_sources: sources
|
|
282
|
+
};
|
|
283
|
+
})();
|
|
284
|
+
}
|
|
285
|
+
// mnfst:realtime.hub:end
|
|
286
|
+
|
|
9
287
|
// Reject strings still holding an unresolved ${VAR} — an undefined env var that
|
|
10
288
|
// would leak verbatim into an Appwrite HTTP header. Loud-fail instead.
|
|
11
289
|
function resolvedOrNull(value, fieldName) {
|
|
@@ -160,6 +438,38 @@ let appwriteAccount = null;
|
|
|
160
438
|
let appwriteTeams = null;
|
|
161
439
|
let appwriteUsers = null;
|
|
162
440
|
|
|
441
|
+
// Legacy subscribe(channel, cb) surface over the shared realtime hub: auth
|
|
442
|
+
// subscriptions (teams/memberships/account) ride the same socket as data and
|
|
443
|
+
// presence, ref-counted and re-bound across identity rebuilds.
|
|
444
|
+
let authRealtimeAdapter = null;
|
|
445
|
+
let authSubSeq = 0;
|
|
446
|
+
function getAuthRealtimeAdapter() {
|
|
447
|
+
if (!window.Appwrite?.Realtime || !window.ManifestRealtimeHub) return null;
|
|
448
|
+
if (authRealtimeAdapter) return authRealtimeAdapter;
|
|
449
|
+
const hub = window.ManifestRealtimeHub;
|
|
450
|
+
const getRealtime = async () => {
|
|
451
|
+
const services = await getAppwriteClient();
|
|
452
|
+
return services?.client ? hub.acquire(services.client) : null;
|
|
453
|
+
};
|
|
454
|
+
authRealtimeAdapter = {
|
|
455
|
+
subscribe(channel, callback) {
|
|
456
|
+
// Unique per call: two subscriptions to one channel keep separate
|
|
457
|
+
// handlers/teardowns (the subKey ref-count still shares the socket sub)
|
|
458
|
+
const name = `auth:${channel}:${++authSubSeq}`;
|
|
459
|
+
hub.subscribe({
|
|
460
|
+
name,
|
|
461
|
+
channel,
|
|
462
|
+
getRealtime,
|
|
463
|
+
// Callers parse response.events themselves; the hub already
|
|
464
|
+
// classified, so this fires once per message
|
|
465
|
+
onEvent: (action, payload, response) => callback(response)
|
|
466
|
+
}).catch(error => console.error('[Manifest Auth] Realtime subscribe failed:', channel, error));
|
|
467
|
+
return () => hub.unsubscribe(name);
|
|
468
|
+
}
|
|
469
|
+
};
|
|
470
|
+
return authRealtimeAdapter;
|
|
471
|
+
}
|
|
472
|
+
|
|
163
473
|
async function getAppwriteClient() {
|
|
164
474
|
if (!window.Appwrite || !window.Appwrite.Client || !window.Appwrite.Account) {
|
|
165
475
|
return null;
|
|
@@ -194,7 +504,7 @@ async function getAppwriteClient() {
|
|
|
194
504
|
teams: appwriteTeams,
|
|
195
505
|
users: appwriteUsers,
|
|
196
506
|
functions: window.Appwrite?.Functions ? new window.Appwrite.Functions(appwriteClient) : null,
|
|
197
|
-
realtime:
|
|
507
|
+
realtime: getAuthRealtimeAdapter() // shared-socket hub adapter, never a second Realtime
|
|
198
508
|
};
|
|
199
509
|
}
|
|
200
510
|
|