mnfst 0.5.254 → 0.5.256

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.
@@ -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: window.Appwrite?.Realtime ? new window.Appwrite.Realtime(appwriteClient) : null
507
+ realtime: getAuthRealtimeAdapter() // shared-socket hub adapter, never a second Realtime
198
508
  };
199
509
  }
200
510