mnfst 0.5.255 → 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.
@@ -2,7 +2,7 @@
2
2
 
3
3
  (function () {
4
4
 
5
- const MANIFEST_BUILD_VERSION = '0.5.255';
5
+ const MANIFEST_BUILD_VERSION = '0.5.256';
6
6
 
7
7
  /* Manifest Data Sources - Appwrite Integration */
8
8
 
@@ -10,6 +10,22 @@ const MANIFEST_BUILD_VERSION = '0.5.255';
10
10
  let appwriteClientCache = null;
11
11
  let appwriteClientPromise = null;
12
12
 
13
+ // Auth plugin's client, polled while configured (script order isn't guaranteed)
14
+ async function resolveAuthPluginClient(authConfigured) {
15
+ const deadline = Date.now() + (authConfigured ? 5000 : 0);
16
+ while (true) {
17
+ if (window.ManifestAppwriteAuthConfig) {
18
+ try {
19
+ const services = await window.ManifestAppwriteAuthConfig.getAppwriteClient();
20
+ if (services?.client) return services.client;
21
+ } catch (e) {
22
+ }
23
+ }
24
+ if (Date.now() >= deadline) return null;
25
+ await new Promise(resolve => setTimeout(resolve, 50));
26
+ }
27
+ }
28
+
13
29
  // Initialize Appwrite client (supports both auth plugin and standalone)
14
30
  async function initializeAppwriteClient() {
15
31
  // Return cached client if available
@@ -30,17 +46,21 @@ async function initializeAppwriteClient() {
30
46
 
31
47
  appwriteClientPromise = (async () => {
32
48
  try {
33
- // Try to get client from auth plugin first (if available)
34
- if (window.ManifestAppwriteAuthConfig) {
35
- try {
36
- const { client } = await window.ManifestAppwriteAuthConfig.getAppwriteClient();
37
- if (client) {
38
- appwriteClientCache = client;
39
- return client;
40
- }
41
- } catch (e) {
42
- }
49
+ // When appwrite.auth is configured the auth plugin's client is
50
+ // canonical: wait (bounded) for its module instead of splitting onto
51
+ // a second Client — a split flips the hub's single Realtime memo
52
+ // back and forth (socket churn on every cross-plugin subscribe)
53
+ let manifest = null;
54
+ let manifestError = null;
55
+ if (window.ManifestDataConfig) {
56
+ try { manifest = await window.ManifestDataConfig.ensureManifest(); } catch (e) { manifestError = e; }
57
+ }
58
+ const authClient = await resolveAuthPluginClient(!!manifest?.appwrite?.auth);
59
+ if (authClient) {
60
+ appwriteClientCache = authClient;
61
+ return authClient;
43
62
  }
63
+ if (manifestError) throw manifestError; // outer catch logs + resets the promise
44
64
 
45
65
  // Fallback: Initialize client from manifest config
46
66
  if (!window.ManifestDataConfig) {
@@ -48,7 +68,6 @@ async function initializeAppwriteClient() {
48
68
  return null;
49
69
  }
50
70
 
51
- const manifest = await window.ManifestDataConfig.ensureManifest();
52
71
  if (!manifest?.appwrite) {
53
72
  console.warn('[Manifest Data] Appwrite config not found in manifest');
54
73
  return null;
@@ -91,18 +110,12 @@ async function initializeAppwriteClient() {
91
110
  return appwriteClientPromise;
92
111
  }
93
112
 
94
- // Shared Realtime: ONE instance (one socket) per tab, keyed to the client it
95
- // wraps. Auth identity events drop it (below) so the next use rebuilds against
96
- // the current session; manifest.data.realtime.js re-binds live channels.
97
- let realtimeCache = null;
98
- let realtimeClient = null;
113
+ // Shared Realtime: ONE instance (one socket) per tab, via the cross-plugin
114
+ // hub (defined in manifest.data.realtime.js and the auth/presences carriers).
115
+ // Auth identity events drop the hub's memo so the next use rebuilds against
116
+ // the current session; the hub re-binds live channels.
99
117
  function getSharedRealtime(client) {
100
- if (!client || !window.Appwrite?.Realtime) return null;
101
- if (!realtimeCache || realtimeClient !== client) {
102
- realtimeCache = new window.Appwrite.Realtime(client);
103
- realtimeClient = client;
104
- }
105
- return realtimeCache;
118
+ return window.ManifestRealtimeHub ? window.ManifestRealtimeHub.acquire(client) : null;
106
119
  }
107
120
 
108
121
  // Get Appwrite data services (TablesDB, Storage, Realtime)
@@ -125,8 +138,7 @@ let authCheck = null;
125
138
  let authVerified = false;
126
139
  ['manifest:auth:login', 'manifest:auth:logout', 'manifest:auth:anonymous', 'manifest:auth:session-cleared', 'manifest:auth:initialized']
127
140
  .forEach(type => window.addEventListener(type, () => {
128
- authCheck = null; authVerified = false;
129
- realtimeCache = null; realtimeClient = null; // socket re-authenticates as the new identity
141
+ authCheck = null; authVerified = false; // the hub drops its own Realtime memo on these events
130
142
  }));
131
143
 
132
144
  async function verifyAuthenticated() {
@@ -683,8 +695,10 @@ window.ManifestDataAppwrite = {
683
695
 
684
696
 
685
697
  /* Manifest Data Sources - Real-time Subscriptions
686
- One shared Realtime client per tab; subscriptions ref-counted by
687
- (channel + canonical server-filter set) across sources.
698
+ One shared Realtime client per tab — via window.ManifestRealtimeHub, shared
699
+ with the auth and presences plugins so N data sources + auth entities +
700
+ presence ride ONE socket. Subscriptions ref-counted by (channel + canonical
701
+ server-filter set) across sources.
688
702
 
689
703
  Appwrite Realtime queries (server-side event filtering), verified against
690
704
  the Web SDK and server source 2026-10:
@@ -706,200 +720,311 @@ window.ManifestDataAppwrite = {
706
720
  expressible filter server-side (such rows then correct on the next load);
707
721
  false sends none. The client matcher stays on in every mode. */
708
722
 
709
- // Map<subKey, { channel, queries, refs, unsubscribe, handlers: Map<sourceName, handler> }>
710
- const entries = new Map();
711
- // Map<sourceName, { subKey, channel, token, handler, getFilters }> — token guards a stale unsubscribe closure
712
- const sources = new Map();
713
- // Map<sourceName, fn(eventType, row)> — pre-filters, run per event per source
714
- const filters = new Map();
715
- // Back-compat/debug surface: Map<sourceName, unsubscribeFunction>
716
- const subscriptions = new Map();
717
- // Map<sourceName, n> — explicit teardowns; aborts a subscribe in flight
718
- const tombstones = new Map();
719
- // Instance the current subscriptions live on
720
- let activeRealtime = null;
721
-
722
- function classify(event) {
723
- if (typeof event !== 'string') return null;
724
- if (event.includes('create')) return 'create';
725
- if (event.includes('update')) return 'update';
726
- if (event.includes('delete')) return 'delete';
727
- return null;
728
- }
723
+ // mnfst:realtime.hub:start
724
+ /* Shared Appwrite Realtime hub — ONE Realtime instance (one socket) per tab
725
+ across the data, auth and presences plugins. The first carrier to load
726
+ defines it; the build syncs this block byte-identical into each carrier
727
+ from scripts/snippets/manifest.realtime.hub.js.
728
+
729
+ Subscriptions are keyed by a caller-chosen name and ref-counted by
730
+ (channel + canonical server-filter key); server-side realtime queries ride
731
+ per subscription where the instance supports them (verified SDK/server
732
+ contract: see the manifest.data.realtime.js header). Messages are
733
+ classified ONCE — Appwrite sends ~20 event-name variants per change —
734
+ so each handler gets one (action, payload, response) call per message. */
735
+ if (!window.ManifestRealtimeHub) {
736
+ window.ManifestRealtimeHub = (function () {
737
+ // Identity events drop the instance memo (the socket re-authenticates
738
+ // as the new session) and re-bind live subscriptions
739
+ const AUTH_EVENTS = ['manifest:auth:login', 'manifest:auth:logout', 'manifest:auth:anonymous', 'manifest:auth:session-cleared', 'manifest:auth:initialized'];
740
+
741
+ // One Realtime (one socket) per tab, keyed to the client it wraps
742
+ let realtimeCache = null;
743
+ let realtimeClient = null;
744
+ function acquire(client) {
745
+ if (!client || !window.Appwrite?.Realtime) return null;
746
+ if (!realtimeCache || realtimeClient !== client) {
747
+ realtimeCache = new window.Appwrite.Realtime(client);
748
+ realtimeClient = client;
749
+ }
750
+ return realtimeCache;
751
+ }
729
752
 
730
- // Query-capable Realtime service (see header): detect per instance, never assume
731
- function supportsQueries(realtime) {
732
- return !!(realtime && (realtime.pendingSubscribes || realtime.slotToSubscriptionId));
733
- }
753
+ // Map<subKey, { channel, queries, refs, unsubscribe, handlers: Map<name, handler> }>
754
+ const entries = new Map();
755
+ // Map<name, { subKey, channel, token, handler, getFilters, getRealtime }>
756
+ const sources = new Map();
757
+ // Map<name, fn(action, payload, response)> — pre-filters, run once per message per name
758
+ const filters = new Map();
759
+ // Map<name, n> — explicit teardowns; aborts a subscribe in flight
760
+ const tombstones = new Map();
761
+ // Instance the current subscriptions live on
762
+ let activeRealtime = null;
763
+
764
+ // Reduce a message's event-name variants to the single most specific
765
+ // action: delete > update (upsert counts: presences) > create
766
+ function classify(events) {
767
+ const list = Array.isArray(events) ? events : (events ? [events] : []);
768
+ let action = null;
769
+ for (const event of list) {
770
+ if (typeof event !== 'string') continue;
771
+ // Verbs match as path segments only — event names embed
772
+ // user-chosen ids (a table named deleted_posts is not a delete)
773
+ const parts = event.split('.');
774
+ if (parts.includes('delete')) return 'delete';
775
+ if (parts.includes('update') || parts.includes('upsert')) action = 'update';
776
+ else if (!action && parts.includes('create')) action = 'create';
777
+ }
778
+ return action;
779
+ }
734
780
 
735
- // (channel, resolved filter set) → stable subscription key; resolved.key is
736
- // already canonical (sorted) so equivalent filter sets share one subscription
737
- function subKeyFor(channel, resolved) {
738
- if (!resolved || !resolved.key) return channel;
739
- return `${channel}\u001f${resolved.key}`;
740
- }
781
+ // Query-capable Realtime service: detect per instance, never assume
782
+ function supportsQueries(realtime) {
783
+ return !!(realtime && (realtime.pendingSubscribes || realtime.slotToSubscriptionId));
784
+ }
741
785
 
742
- // Fan one SDK message out: each registered source handler gets each event once,
743
- // after that source's pre-filter (setFilter) — before any merge/dedup work
744
- function dispatch(subKey, response) {
745
- const entry = entries.get(subKey);
746
- if (!entry || !response || !response.events) return;
747
- const events = Array.isArray(response.events) ? response.events : [response.events];
748
- let payload = response.payload;
749
- if (!payload || (!payload.$id && !payload.row)) payload = response;
750
- for (const event of events) {
751
- const type = classify(event);
752
- if (!type) continue;
753
- for (const [name, handler] of entry.handlers) {
754
- const filter = filters.get(name);
755
- if (filter && filter(type, payload) === false) continue;
756
- handler(type, payload);
786
+ // (channel, resolved filter set) → stable subscription key; resolved.key
787
+ // is already canonical so equivalent filter sets share one subscription
788
+ function subKeyFor(channel, resolved) {
789
+ if (!resolved || !resolved.key) return channel;
790
+ return `${channel}\u001f${resolved.key}`;
757
791
  }
758
- }
759
- }
760
792
 
761
- // subscribe() returns an unsubscribe function (legacy client) or a
762
- // Promise<RealtimeSubscription> (Realtime service): normalize teardown
763
- function dropSub(sub) {
764
- if (typeof sub === 'function') sub();
765
- else if (sub && typeof sub.unsubscribe === 'function') sub.unsubscribe(); // per-subscription; never risks the shared socket
766
- else if (sub && typeof sub.close === 'function') sub.close();
767
- }
793
+ // Fan one SDK message out, classified once: each registered handler gets
794
+ // ONE (action, payload, response) call per message, after that name's
795
+ // pre-filter (setFilter) — before any merge/dedup work
796
+ function dispatch(subKey, response) {
797
+ const entry = entries.get(subKey);
798
+ if (!entry || !response || !response.events) return;
799
+ const action = classify(response.events);
800
+ if (!action) return;
801
+ let payload = response.payload;
802
+ if (!payload || (!payload.$id && !payload.row)) payload = response;
803
+ for (const [name, handler] of entry.handlers) {
804
+ const filter = filters.get(name);
805
+ if (filter && filter(action, payload, response) === false) continue;
806
+ handler(action, payload, response);
807
+ }
808
+ }
768
809
 
769
- function makeUnsubscriber(result) {
770
- if (!result || typeof result.then !== 'function') {
771
- return () => { try { dropSub(result); } catch (e) { /* socket already gone */ } };
772
- }
773
- const settled = result.catch(() => null);
774
- return () => { settled.then(dropSub).catch(() => { /* socket already gone */ }); };
775
- }
810
+ // subscribe() returns an unsubscribe function (legacy client) or a
811
+ // Promise<RealtimeSubscription> (Realtime service): normalize teardown
812
+ function dropSub(sub) {
813
+ if (typeof sub === 'function') sub();
814
+ else if (sub && typeof sub.unsubscribe === 'function') sub.unsubscribe(); // per-subscription; never risks the shared socket
815
+ else if (sub && typeof sub.close === 'function') sub.close();
816
+ }
776
817
 
777
- function sdkSubscribe(realtime, subKey, channel, queries) {
778
- const cb = response => dispatch(subKey, response);
779
- const result = (queries && queries.length)
780
- ? realtime.subscribe(channel, cb, queries)
781
- : realtime.subscribe(channel, cb);
782
- return makeUnsubscriber(result);
783
- }
818
+ function makeUnsubscriber(result) {
819
+ if (!result || typeof result.then !== 'function') {
820
+ return () => { try { dropSub(result); } catch (e) { /* socket already gone */ } };
821
+ }
822
+ const settled = result.catch(() => null);
823
+ return () => { settled.then(dropSub).catch(() => { /* socket already gone */ }); };
824
+ }
784
825
 
785
- // Get or create the shared entry for (channel, filters); registered in the map
786
- // before the SDK call so an event arriving mid-subscribe already dispatches
787
- function ensureEntry(realtime, subKey, channel, queries) {
788
- let entry = entries.get(subKey);
789
- if (entry) return entry;
790
- entry = { channel, queries: queries || [], refs: 0, unsubscribe: null, handlers: new Map() };
791
- entries.set(subKey, entry);
792
- try {
793
- entry.unsubscribe = sdkSubscribe(realtime, subKey, channel, queries);
794
- } catch (error) {
795
- entries.delete(subKey);
796
- throw error;
797
- }
798
- return entry;
799
- }
826
+ function sdkSubscribe(realtime, subKey, channel, queries) {
827
+ const cb = response => dispatch(subKey, response);
828
+ const result = (queries && queries.length)
829
+ ? realtime.subscribe(channel, cb, queries)
830
+ : realtime.subscribe(channel, cb);
831
+ return makeUnsubscriber(result);
832
+ }
800
833
 
801
- function removeFromEntry(name, subKey) {
802
- const entry = entries.get(subKey);
803
- if (!entry) return;
804
- entry.handlers.delete(name);
805
- entry.refs = entry.handlers.size;
806
- if (entry.refs === 0) {
807
- entries.delete(subKey);
808
- if (typeof entry.unsubscribe === 'function') {
809
- try { entry.unsubscribe(); } catch (e) { /* socket already gone */ }
834
+ // Get or create the shared entry for (channel, filters); registered in the
835
+ // map before the SDK call so an event arriving mid-subscribe already dispatches
836
+ function ensureEntry(realtime, subKey, channel, queries) {
837
+ let entry = entries.get(subKey);
838
+ if (entry) return entry;
839
+ entry = { channel, queries: queries || [], refs: 0, unsubscribe: null, handlers: new Map() };
840
+ entries.set(subKey, entry);
841
+ try {
842
+ entry.unsubscribe = sdkSubscribe(realtime, subKey, channel, queries);
843
+ } catch (error) {
844
+ entries.delete(subKey);
845
+ throw error;
846
+ }
847
+ return entry;
810
848
  }
811
- }
812
- }
813
849
 
814
- // Drop a source's registration; tear the subscription down when the last source leaves
815
- function detach(name, token) {
816
- const cur = sources.get(name);
817
- if (!cur || (token && cur.token !== token)) return;
818
- sources.delete(name);
819
- removeFromEntry(name, cur.subKey);
820
- }
850
+ function removeFromEntry(name, subKey) {
851
+ const entry = entries.get(subKey);
852
+ if (!entry) return;
853
+ entry.handlers.delete(name);
854
+ entry.refs = entry.handlers.size;
855
+ if (entry.refs === 0) {
856
+ entries.delete(subKey);
857
+ if (typeof entry.unsubscribe === 'function') {
858
+ try { entry.unsubscribe(); } catch (e) { /* socket already gone */ }
859
+ }
860
+ }
861
+ }
821
862
 
822
- // Instance changed (client rebuilt on identity change): re-resolve each source's
823
- // filters ($auth. values are stale) and regroup subscriptions on the new instance.
824
- // Chained so overlapping rebuilds run in order; a superseded rebuild stops.
825
- let syncChain = Promise.resolve();
826
- function syncInstance(realtime) {
827
- if (activeRealtime === realtime) return syncChain;
828
- activeRealtime = realtime;
829
- syncChain = syncChain
830
- .then(() => rebindAll(realtime))
831
- .catch(error => console.error('[Manifest Data] Realtime rebind failed:', error));
832
- return syncChain;
833
- }
863
+ // Drop a name's registration; tear the subscription down when the last one leaves
864
+ function detach(name, token) {
865
+ const cur = sources.get(name);
866
+ if (!cur || (token && cur.token !== token)) return;
867
+ sources.delete(name);
868
+ removeFromEntry(name, cur.subKey);
869
+ }
834
870
 
835
- async function rebindAll(realtime) {
836
- for (const [, entry] of entries) {
837
- if (typeof entry.unsubscribe === 'function') {
838
- try { entry.unsubscribe(); } catch (e) { /* old instance */ }
871
+ // Instance changed (rebuilt on identity change): re-resolve each name's
872
+ // filters ($auth. values are stale) and regroup subscriptions on the new
873
+ // instance. Chained so overlapping rebuilds run in order; superseded stops.
874
+ let syncChain = Promise.resolve();
875
+ function syncInstance(realtime) {
876
+ if (activeRealtime === realtime) return syncChain;
877
+ activeRealtime = realtime;
878
+ syncChain = syncChain
879
+ .then(() => rebindAll(realtime))
880
+ .catch(error => console.error('[Manifest Realtime] Rebind failed:', error));
881
+ return syncChain;
839
882
  }
840
- }
841
- entries.clear();
842
- const canQuery = supportsQueries(realtime);
843
- for (const [name, src] of sources) {
844
- if (activeRealtime !== realtime) return; // superseded by a newer instance
845
- const resolved = (canQuery && src.getFilters) ? await src.getFilters() : null;
846
- const subKey = subKeyFor(src.channel, resolved);
847
- try {
848
- const entry = ensureEntry(realtime, subKey, src.channel, resolved?.queries);
849
- entry.handlers.set(name, src.handler);
883
+
884
+ async function rebindAll(realtime) {
885
+ for (const [, entry] of entries) {
886
+ if (typeof entry.unsubscribe === 'function') {
887
+ try { entry.unsubscribe(); } catch (e) { /* old instance */ }
888
+ }
889
+ }
890
+ entries.clear();
891
+ const canQuery = supportsQueries(realtime);
892
+ for (const [name, src] of sources) {
893
+ if (activeRealtime !== realtime) return; // superseded by a newer instance
894
+ try {
895
+ // Inside the try: one source's failing getFilters must not
896
+ // strand the sources after it unsubscribed
897
+ const resolved = (canQuery && src.getFilters) ? await src.getFilters() : null;
898
+ if (activeRealtime !== realtime) return;
899
+ const subKey = subKeyFor(src.channel, resolved);
900
+ const entry = ensureEntry(realtime, subKey, src.channel, resolved?.queries);
901
+ entry.handlers.set(name, src.handler);
902
+ entry.refs = entry.handlers.size;
903
+ src.subKey = subKey;
904
+ } catch (error) {
905
+ console.error('[Manifest Realtime] Failed to re-subscribe channel:', src.channel, error);
906
+ }
907
+ }
908
+ }
909
+
910
+ // One SDK subscribe per distinct (channel, filter set); names sharing both
911
+ // share it. getRealtime: async () => instance (re-run on identity rebuilds).
912
+ // isCurrent: false once the requesting load is superseded — skip, never
913
+ // touch a newer entry. getFilters: async () => ({ queries, key }) | null,
914
+ // re-run per (re)subscribe; null → channel-only subscribe. A replaced
915
+ // registration detaches only after the new subscription is live (no event
916
+ // gap); overlap double-delivery is the consumer's dedupe to absorb.
917
+ async function subscribe({ name, channel, onEvent, isCurrent, getFilters, getRealtime }) {
918
+ const epoch = tombstones.get(name) || 0;
919
+ const realtime = getRealtime ? await getRealtime() : null;
920
+ if (isCurrent && !isCurrent()) return null;
921
+ if (!realtime) {
922
+ detach(name);
923
+ return null;
924
+ }
925
+ await syncInstance(realtime);
926
+ if (isCurrent && !isCurrent()) return null;
927
+
928
+ const resolved = (supportsQueries(realtime) && getFilters) ? await getFilters() : null;
929
+ if (isCurrent && !isCurrent()) return null;
930
+ // Torn down while resolving: land nothing
931
+ if ((tombstones.get(name) || 0) !== epoch) return null;
932
+ // Instance replaced while resolving (identity rebuild): re-run against the live one
933
+ if (activeRealtime !== realtime) return subscribe({ name, channel, onEvent, isCurrent, getFilters, getRealtime });
934
+ const subKey = subKeyFor(channel, resolved);
935
+
936
+ const prev = sources.get(name);
937
+ let entry;
938
+ try {
939
+ entry = ensureEntry(realtime, subKey, channel, resolved?.queries);
940
+ } catch (error) {
941
+ console.error('[Manifest Realtime] Failed to subscribe:', channel, error);
942
+ detach(name);
943
+ return null;
944
+ }
945
+ const token = Symbol(name);
946
+ entry.handlers.set(name, onEvent);
850
947
  entry.refs = entry.handlers.size;
851
- src.subKey = subKey;
852
- } catch (error) {
853
- console.error('[Manifest Data] Failed to re-subscribe channel:', src.channel, error);
948
+ sources.set(name, { subKey, channel, token, handler: onEvent, getFilters, getRealtime });
949
+ if (prev && prev.subKey !== subKey) removeFromEntry(name, prev.subKey);
950
+
951
+ return () => detach(name, token);
854
952
  }
855
- }
953
+
954
+ // Explicit teardown by name; also aborts a subscribe in flight and
955
+ // drops the name's pre-filter (a replacement re-registers its own)
956
+ function unsubscribe(name) {
957
+ tombstones.set(name, (tombstones.get(name) || 0) + 1);
958
+ filters.delete(name);
959
+ detach(name);
960
+ }
961
+
962
+ // Programmatic pre-filter: fn(action, payload, response) runs once per
963
+ // message per name BEFORE the handler; false drops it. null clears.
964
+ function setFilter(name, fn) {
965
+ if (fn == null) filters.delete(name);
966
+ else if (typeof fn === 'function') filters.set(name, fn);
967
+ }
968
+
969
+ // Identity events: drop the memo, then re-bind live subscriptions on a
970
+ // fresh instance from any registered source's getter
971
+ if (typeof window !== 'undefined') {
972
+ AUTH_EVENTS.forEach(type => window.addEventListener(type, async () => {
973
+ realtimeCache = null;
974
+ realtimeClient = null;
975
+ if (!sources.size) return;
976
+ try {
977
+ const src = [...sources.values()].find(s => s.getRealtime);
978
+ const realtime = src ? await src.getRealtime() : null;
979
+ if (realtime) syncInstance(realtime);
980
+ } catch (error) {
981
+ console.error('[Manifest Realtime] Identity rebuild failed:', error);
982
+ }
983
+ }));
984
+ }
985
+
986
+ return {
987
+ AUTH_EVENTS,
988
+ acquire,
989
+ subscribe,
990
+ unsubscribe,
991
+ setFilter,
992
+ classify,
993
+ supportsQueries,
994
+ _entries: entries, // Expose for debugging (keyed by channel + filter set)
995
+ _sources: sources
996
+ };
997
+ })();
856
998
  }
999
+ // mnfst:realtime.hub:end
1000
+
1001
+ // Back-compat/debug surface: Map<sourceName, unsubscribeFunction>
1002
+ const subscriptions = new Map();
857
1003
 
858
- // One SDK subscribe per distinct (channel, filter set); sources sharing both share it.
859
- // isCurrent: false once the requesting load is superseded (logout) — skip, never touch a newer entry.
860
- // getFilters: async () => ({ queries, key }) | null, re-run per (re)subscribe so
1004
+ // One SDK subscribe per distinct (channel, filter set), delegated to the hub;
1005
+ // names are the data-source names. getFilters re-runs per (re)subscribe so
861
1006
  // $auth. values resolve at subscribe time; null → channel-only (0.5.248 path).
862
- // A replaced registration detaches only after the new subscription is live (no
863
- // event gap); overlap double-delivery is absorbed by the per-source event dedupe.
864
1007
  async function subscribeChannel(dataSourceName, channel, onEvent, isCurrent, getFilters) {
865
- const epoch = tombstones.get(dataSourceName) || 0;
866
- const services = await window.ManifestDataAppwrite.getAppwriteDataServices();
867
- if (isCurrent && !isCurrent()) return null;
868
- if (!services?.realtime) {
869
- console.warn('[Manifest Data] Realtime service not available for', dataSourceName);
870
- detach(dataSourceName);
871
- subscriptions.delete(dataSourceName);
872
- return null;
873
- }
874
- const realtime = services.realtime;
875
- await syncInstance(realtime);
876
- if (isCurrent && !isCurrent()) return null;
877
-
878
- const resolved = (supportsQueries(realtime) && getFilters) ? await getFilters() : null;
879
- if (isCurrent && !isCurrent()) return null;
880
- // Torn down while resolving: land nothing
881
- if ((tombstones.get(dataSourceName) || 0) !== epoch) return null;
882
- // Instance replaced while resolving (identity rebuild): re-run against the live one
883
- if (activeRealtime !== realtime) return subscribeChannel(dataSourceName, channel, onEvent, isCurrent, getFilters);
884
- const subKey = subKeyFor(channel, resolved);
885
-
886
- const prev = sources.get(dataSourceName);
887
- let entry;
888
- try {
889
- entry = ensureEntry(realtime, subKey, channel, resolved?.queries);
890
- } catch (error) {
891
- console.error('[Manifest Data] Failed to subscribe:', channel, error);
892
- detach(dataSourceName);
1008
+ const getRealtime = async () => {
1009
+ const services = await window.ManifestDataAppwrite.getAppwriteDataServices();
1010
+ if (!services?.realtime) {
1011
+ console.warn('[Manifest Data] Realtime service not available for', dataSourceName);
1012
+ return null;
1013
+ }
1014
+ return services.realtime;
1015
+ };
1016
+ const unsubscribe = await window.ManifestRealtimeHub.subscribe({
1017
+ name: dataSourceName,
1018
+ channel,
1019
+ onEvent,
1020
+ isCurrent,
1021
+ getFilters,
1022
+ getRealtime
1023
+ });
1024
+ if (!unsubscribe) {
893
1025
  subscriptions.delete(dataSourceName);
894
1026
  return null;
895
1027
  }
896
- const token = Symbol(dataSourceName);
897
- entry.handlers.set(dataSourceName, onEvent);
898
- entry.refs = entry.handlers.size;
899
- sources.set(dataSourceName, { subKey, channel, token, handler: onEvent, getFilters });
900
- if (prev && prev.subKey !== subKey) removeFromEntry(dataSourceName, prev.subKey);
901
-
902
- const unsubscribe = () => detach(dataSourceName, token);
903
1028
  subscriptions.set(dataSourceName, unsubscribe);
904
1029
  return unsubscribe;
905
1030
  }
@@ -924,36 +1049,22 @@ function subscribeToTable(dataSourceName, databaseId, tableId, scope, onEvent, i
924
1049
 
925
1050
  // Unsubscribe from a data source
926
1051
  function unsubscribeFromDataSource(dataSourceName) {
927
- tombstones.set(dataSourceName, (tombstones.get(dataSourceName) || 0) + 1);
928
- detach(dataSourceName);
1052
+ window.ManifestRealtimeHub.unsubscribe(dataSourceName);
929
1053
  subscriptions.delete(dataSourceName);
930
1054
  }
931
1055
 
932
- // Unsubscribe from all data sources
1056
+ // Unsubscribe from all data sources (never touches auth/presence registrations)
933
1057
  function unsubscribeAll() {
934
- for (const name of [...sources.keys()]) {
935
- tombstones.set(name, (tombstones.get(name) || 0) + 1);
936
- detach(name);
1058
+ for (const name of [...subscriptions.keys()]) {
1059
+ window.ManifestRealtimeHub.unsubscribe(name);
937
1060
  }
938
1061
  subscriptions.clear();
939
1062
  }
940
1063
 
941
- // Programmatic pre-filter: fn(eventType, row) runs per event per source BEFORE
942
- // any merge/dedup work; returning false drops the event. null clears it.
1064
+ // Programmatic pre-filter: fn(action, row) runs once per message per source
1065
+ // BEFORE any merge/dedup work; returning false drops the event. null clears it.
943
1066
  function setFilter(sourceName, fn) {
944
- if (fn == null) filters.delete(sourceName);
945
- else if (typeof fn === 'function') filters.set(sourceName, fn);
946
- }
947
-
948
- // Identity events rebuild the shared Realtime (manifest.data.appwrite.js drops
949
- // its memo on the same events); re-bind live subscriptions to the new instance
950
- if (typeof window !== 'undefined') {
951
- ['manifest:auth:login', 'manifest:auth:logout', 'manifest:auth:anonymous', 'manifest:auth:session-cleared', 'manifest:auth:initialized']
952
- .forEach(type => window.addEventListener(type, async () => {
953
- if (!sources.size) return;
954
- const services = await window.ManifestDataAppwrite?.getAppwriteDataServices?.();
955
- if (services?.realtime) syncInstance(services.realtime);
956
- }));
1067
+ window.ManifestRealtimeHub.setFilter(sourceName, fn);
957
1068
  }
958
1069
 
959
1070
  // Export functions
@@ -964,7 +1075,7 @@ window.ManifestDataRealtime = {
964
1075
  unsubscribeAll,
965
1076
  setFilter,
966
1077
  subscriptions, // Expose for debugging
967
- _channels: entries // Expose for debugging (keyed by channel + filter set)
1078
+ _channels: window.ManifestRealtimeHub._entries // Expose for debugging (keyed by channel + filter set)
968
1079
  };
969
1080
 
970
1081