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.
- 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.data.js +1 -1
- package/lib/manifest.integrity.json +5 -5
- package/lib/manifest.schema.json +17 -0
- package/lib/manifest.sw.js +1 -1
- package/package.json +1 -1
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
(function () {
|
|
4
4
|
|
|
5
|
-
const MANIFEST_BUILD_VERSION = '0.5.
|
|
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
|
-
//
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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,
|
|
95
|
-
//
|
|
96
|
-
// the
|
|
97
|
-
|
|
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
|
-
|
|
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
|
|
687
|
-
|
|
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
|
-
//
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
function
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
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
|
-
//
|
|
731
|
-
|
|
732
|
-
|
|
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
|
-
//
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
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
|
-
//
|
|
743
|
-
//
|
|
744
|
-
function
|
|
745
|
-
|
|
746
|
-
|
|
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
|
-
//
|
|
762
|
-
//
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
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
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
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
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
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
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
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
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
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
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
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
|
-
//
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
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
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
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
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
const
|
|
849
|
-
|
|
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
|
-
|
|
852
|
-
|
|
853
|
-
|
|
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)
|
|
859
|
-
//
|
|
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
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
if (
|
|
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
|
-
|
|
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 [...
|
|
935
|
-
|
|
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(
|
|
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
|
-
|
|
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:
|
|
1078
|
+
_channels: window.ManifestRealtimeHub._entries // Expose for debugging (keyed by channel + filter set)
|
|
968
1079
|
};
|
|
969
1080
|
|
|
970
1081
|
|