mnfst 0.5.247 → 0.5.249
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.data.js +379 -138
- package/lib/manifest.data.js +165 -33
- package/lib/manifest.integrity.json +4 -4
- package/lib/manifest.js +2 -2
- package/lib/manifest.schema.json +431 -97
- 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.249';
|
|
6
6
|
|
|
7
7
|
/* Manifest Data Sources - Appwrite Integration */
|
|
8
8
|
|
|
@@ -91,6 +91,20 @@ async function initializeAppwriteClient() {
|
|
|
91
91
|
return appwriteClientPromise;
|
|
92
92
|
}
|
|
93
93
|
|
|
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;
|
|
99
|
+
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;
|
|
106
|
+
}
|
|
107
|
+
|
|
94
108
|
// Get Appwrite data services (TablesDB, Storage, Realtime)
|
|
95
109
|
// Works with or without auth plugin
|
|
96
110
|
async function getAppwriteDataServices() {
|
|
@@ -102,7 +116,7 @@ async function getAppwriteDataServices() {
|
|
|
102
116
|
return {
|
|
103
117
|
tablesDB: window.Appwrite.TablesDB ? new window.Appwrite.TablesDB(client) : null,
|
|
104
118
|
storage: window.Appwrite.Storage ? new window.Appwrite.Storage(client) : null,
|
|
105
|
-
realtime:
|
|
119
|
+
realtime: getSharedRealtime(client)
|
|
106
120
|
};
|
|
107
121
|
}
|
|
108
122
|
|
|
@@ -110,7 +124,10 @@ async function getAppwriteDataServices() {
|
|
|
110
124
|
let authCheck = null;
|
|
111
125
|
let authVerified = false;
|
|
112
126
|
['manifest:auth:login', 'manifest:auth:logout', 'manifest:auth:anonymous', 'manifest:auth:session-cleared', 'manifest:auth:initialized']
|
|
113
|
-
.forEach(type => window.addEventListener(type, () => {
|
|
127
|
+
.forEach(type => window.addEventListener(type, () => {
|
|
128
|
+
authCheck = null; authVerified = false;
|
|
129
|
+
realtimeCache = null; realtimeClient = null; // socket re-authenticates as the new identity
|
|
130
|
+
}));
|
|
114
131
|
|
|
115
132
|
async function verifyAuthenticated() {
|
|
116
133
|
if (!window.ManifestAppwriteAuthConfig || authVerified) return;
|
|
@@ -665,159 +682,278 @@ window.ManifestDataAppwrite = {
|
|
|
665
682
|
};
|
|
666
683
|
|
|
667
684
|
|
|
668
|
-
/* Manifest Data Sources - Real-time Subscriptions
|
|
685
|
+
/* 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.
|
|
688
|
+
|
|
689
|
+
Appwrite Realtime queries (server-side event filtering), verified against
|
|
690
|
+
the Web SDK and server source 2026-10:
|
|
691
|
+
- Realtime#subscribe(channels, callback, queries?) — queries are per
|
|
692
|
+
subscription (Query strings, ANDed; OR across subscriptions), supported
|
|
693
|
+
ops: equal/notEqual/lessThan(Equal)/greaterThan(Equal)/isNull/isNotNull/
|
|
694
|
+
and/or. SDK >=25.0.0 sends {type:'subscribe', data:[{subscriptionId,
|
|
695
|
+
channels, queries}]} socket messages and dispatches by the matched
|
|
696
|
+
subscription ids the server echoes; 22.4-24 carried per-slot queries in
|
|
697
|
+
the socket URL. subscribe() resolves to {unsubscribe, update, close}.
|
|
698
|
+
- Feature detection: a query-capable instance has pendingSubscribes (>=25)
|
|
699
|
+
or slotToSubscriptionId (22.4-24). Without either marker: channel-only
|
|
700
|
+
subscribe, exactly the 0.5.248 path.
|
|
701
|
+
- The server evaluates queries against the event payload only, so an
|
|
702
|
+
update that makes a held row STOP matching is not delivered. Therefore
|
|
703
|
+
serverFilter defaults to 'scope': only scope-column filters (permission-
|
|
704
|
+
mirrored, effectively immutable) ride the subscription; user queries stay
|
|
705
|
+
client-side so stop-matching rows still drop live. 'all' pushes every
|
|
706
|
+
expressible filter server-side (such rows then correct on the next load);
|
|
707
|
+
false sends none. The client matcher stays on in every mode. */
|
|
708
|
+
|
|
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
|
+
}
|
|
669
729
|
|
|
670
|
-
//
|
|
671
|
-
|
|
730
|
+
// Query-capable Realtime service (see header): detect per instance, never assume
|
|
731
|
+
function supportsQueries(realtime) {
|
|
732
|
+
return !!(realtime && (realtime.pendingSubscribes || realtime.slotToSubscriptionId));
|
|
733
|
+
}
|
|
672
734
|
|
|
673
|
-
//
|
|
674
|
-
//
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
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
|
+
}
|
|
741
|
+
|
|
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);
|
|
681
757
|
}
|
|
682
|
-
subscriptions.delete(dataSourceName);
|
|
683
758
|
}
|
|
759
|
+
}
|
|
684
760
|
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
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
|
+
}
|
|
691
768
|
|
|
692
|
-
|
|
693
|
-
|
|
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
|
+
}
|
|
694
776
|
|
|
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
|
+
}
|
|
695
784
|
|
|
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);
|
|
696
792
|
try {
|
|
697
|
-
|
|
698
|
-
if (!response || !response.events) {
|
|
699
|
-
return;
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
// Handle both array and single event formats
|
|
703
|
-
const events = Array.isArray(response.events) ? response.events : [response.events];
|
|
704
|
-
|
|
705
|
-
events.forEach(event => {
|
|
706
|
-
if (typeof event !== 'string') return;
|
|
707
|
-
|
|
708
|
-
// Payload might be in response.payload or response directly
|
|
709
|
-
const payload = response.payload || response;
|
|
710
|
-
|
|
711
|
-
// Handle different event types
|
|
712
|
-
if (event.includes('create') || event.includes('storage.files.create')) {
|
|
713
|
-
// New file created
|
|
714
|
-
onEvent('create', payload);
|
|
715
|
-
} else if (event.includes('update') || event.includes('storage.files.update')) {
|
|
716
|
-
// File updated
|
|
717
|
-
onEvent('update', payload);
|
|
718
|
-
} else if (event.includes('delete') || event.includes('storage.files.delete')) {
|
|
719
|
-
// File deleted
|
|
720
|
-
onEvent('delete', payload);
|
|
721
|
-
}
|
|
722
|
-
});
|
|
723
|
-
});
|
|
724
|
-
|
|
725
|
-
subscriptions.set(dataSourceName, unsubscribe);
|
|
726
|
-
|
|
727
|
-
return unsubscribe;
|
|
793
|
+
entry.unsubscribe = sdkSubscribe(realtime, subKey, channel, queries);
|
|
728
794
|
} catch (error) {
|
|
729
|
-
|
|
730
|
-
|
|
795
|
+
entries.delete(subKey);
|
|
796
|
+
throw error;
|
|
731
797
|
}
|
|
798
|
+
return entry;
|
|
732
799
|
}
|
|
733
800
|
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
if (
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
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 */ }
|
|
740
810
|
}
|
|
741
|
-
subscriptions.delete(dataSourceName);
|
|
742
811
|
}
|
|
743
812
|
}
|
|
744
813
|
|
|
745
|
-
//
|
|
746
|
-
function
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
});
|
|
752
|
-
subscriptions.clear();
|
|
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);
|
|
753
820
|
}
|
|
754
821
|
|
|
755
|
-
//
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
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
|
+
}
|
|
834
|
+
|
|
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 */ }
|
|
839
|
+
}
|
|
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);
|
|
850
|
+
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);
|
|
762
854
|
}
|
|
763
|
-
subscriptions.delete(dataSourceName);
|
|
764
855
|
}
|
|
856
|
+
}
|
|
765
857
|
|
|
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
|
|
861
|
+
// $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
|
+
async function subscribeChannel(dataSourceName, channel, onEvent, isCurrent, getFilters) {
|
|
865
|
+
const epoch = tombstones.get(dataSourceName) || 0;
|
|
766
866
|
const services = await window.ManifestDataAppwrite.getAppwriteDataServices();
|
|
767
867
|
if (isCurrent && !isCurrent()) return null;
|
|
768
868
|
if (!services?.realtime) {
|
|
769
869
|
console.warn('[Manifest Data] Realtime service not available for', dataSourceName);
|
|
870
|
+
detach(dataSourceName);
|
|
871
|
+
subscriptions.delete(dataSourceName);
|
|
770
872
|
return null;
|
|
771
873
|
}
|
|
874
|
+
const realtime = services.realtime;
|
|
875
|
+
await syncInstance(realtime);
|
|
876
|
+
if (isCurrent && !isCurrent()) return null;
|
|
772
877
|
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
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;
|
|
776
888
|
try {
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
889
|
+
entry = ensureEntry(realtime, subKey, channel, resolved?.queries);
|
|
890
|
+
} catch (error) {
|
|
891
|
+
console.error('[Manifest Data] Failed to subscribe:', channel, error);
|
|
892
|
+
detach(dataSourceName);
|
|
893
|
+
subscriptions.delete(dataSourceName);
|
|
894
|
+
return null;
|
|
895
|
+
}
|
|
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);
|
|
784
901
|
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
902
|
+
const unsubscribe = () => detach(dataSourceName, token);
|
|
903
|
+
subscriptions.set(dataSourceName, unsubscribe);
|
|
904
|
+
return unsubscribe;
|
|
905
|
+
}
|
|
789
906
|
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
907
|
+
// Resolver re-runs per (re)subscribe so $auth. values are current at that moment
|
|
908
|
+
function filterResolver(filterSpec) {
|
|
909
|
+
if (!filterSpec) return null;
|
|
910
|
+
const build = window.ManifestDataQueries?.buildRealtimeQueries;
|
|
911
|
+
if (typeof build !== 'function') return null;
|
|
912
|
+
return () => build(filterSpec.queriesConfig, filterSpec.scope, filterSpec.scopeColumns, filterSpec.mode);
|
|
913
|
+
}
|
|
793
914
|
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
915
|
+
// Subscribe to storage bucket file changes
|
|
916
|
+
function subscribeToStorageBucket(dataSourceName, bucketId, scope, onEvent, isCurrent, filterSpec) {
|
|
917
|
+
return subscribeChannel(dataSourceName, `buckets.${bucketId}.files`, onEvent, isCurrent, filterResolver(filterSpec));
|
|
918
|
+
}
|
|
798
919
|
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
} else if (event.includes('update') || event.includes('rows.update') || event.includes('documents.update')) {
|
|
804
|
-
// Row updated
|
|
805
|
-
onEvent('update', payload);
|
|
806
|
-
} else if (event.includes('delete') || event.includes('rows.delete') || event.includes('documents.delete')) {
|
|
807
|
-
// Row deleted
|
|
808
|
-
onEvent('delete', payload);
|
|
809
|
-
} else {
|
|
810
|
-
}
|
|
811
|
-
});
|
|
812
|
-
});
|
|
920
|
+
// Subscribe to database table row changes
|
|
921
|
+
function subscribeToTable(dataSourceName, databaseId, tableId, scope, onEvent, isCurrent, filterSpec) {
|
|
922
|
+
return subscribeChannel(dataSourceName, `databases.${databaseId}.tables.${tableId}.rows`, onEvent, isCurrent, filterResolver(filterSpec));
|
|
923
|
+
}
|
|
813
924
|
|
|
814
|
-
|
|
925
|
+
// Unsubscribe from a data source
|
|
926
|
+
function unsubscribeFromDataSource(dataSourceName) {
|
|
927
|
+
tombstones.set(dataSourceName, (tombstones.get(dataSourceName) || 0) + 1);
|
|
928
|
+
detach(dataSourceName);
|
|
929
|
+
subscriptions.delete(dataSourceName);
|
|
930
|
+
}
|
|
815
931
|
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
932
|
+
// Unsubscribe from all data sources
|
|
933
|
+
function unsubscribeAll() {
|
|
934
|
+
for (const name of [...sources.keys()]) {
|
|
935
|
+
tombstones.set(name, (tombstones.get(name) || 0) + 1);
|
|
936
|
+
detach(name);
|
|
820
937
|
}
|
|
938
|
+
subscriptions.clear();
|
|
939
|
+
}
|
|
940
|
+
|
|
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.
|
|
943
|
+
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
|
+
}));
|
|
821
957
|
}
|
|
822
958
|
|
|
823
959
|
// Export functions
|
|
@@ -826,7 +962,9 @@ window.ManifestDataRealtime = {
|
|
|
826
962
|
subscribeToTable,
|
|
827
963
|
unsubscribeFromDataSource,
|
|
828
964
|
unsubscribeAll,
|
|
829
|
-
|
|
965
|
+
setFilter,
|
|
966
|
+
subscriptions, // Expose for debugging
|
|
967
|
+
_channels: entries // Expose for debugging (keyed by channel + filter set)
|
|
830
968
|
};
|
|
831
969
|
|
|
832
970
|
|
|
@@ -1186,6 +1324,86 @@ async function buildQueries(queriesConfig, scope, scopeColumns) {
|
|
|
1186
1324
|
return [...scopeQueries, ...userQueries];
|
|
1187
1325
|
}
|
|
1188
1326
|
|
|
1327
|
+
// Query methods buildAppwriteQueries serializes (anything else is dropped at
|
|
1328
|
+
// load and must therefore never drop rows in the realtime matcher below)
|
|
1329
|
+
const QUERY_MAP = {
|
|
1330
|
+
'equal': 'equal',
|
|
1331
|
+
'notEqual': 'notEqual',
|
|
1332
|
+
'lessThan': 'lessThan',
|
|
1333
|
+
'lessThanEqual': 'lessThanEqual',
|
|
1334
|
+
'greaterThan': 'greaterThan',
|
|
1335
|
+
'greaterThanEqual': 'greaterThanEqual',
|
|
1336
|
+
'contains': 'contains',
|
|
1337
|
+
'search': 'search',
|
|
1338
|
+
'or': 'or', // Support for multi-team queries (Query.or([Query.equal(...), ...]))
|
|
1339
|
+
'orderAsc': 'orderAsc',
|
|
1340
|
+
'orderDesc': 'orderDesc',
|
|
1341
|
+
'limit': 'limit',
|
|
1342
|
+
'offset': 'offset',
|
|
1343
|
+
'cursorAfter': 'cursorAfter',
|
|
1344
|
+
'cursorBefore': 'cursorBefore'
|
|
1345
|
+
};
|
|
1346
|
+
|
|
1347
|
+
// Client-side row matcher for realtime events: evaluates a source's configured
|
|
1348
|
+
// filter queries against one row. Only operators the load actually serializes
|
|
1349
|
+
// (QUERY_MAP) are evaluable. Liveness over precision: anything unevaluable —
|
|
1350
|
+
// search, an operator the load drops, or a $auth. arg that can't resolve
|
|
1351
|
+
// synchronously — counts as a MATCH so live events are never silently dropped.
|
|
1352
|
+
const NON_FILTER_METHODS = new Set(['limit', 'offset', 'orderAsc', 'orderDesc', 'select', 'cursorAfter', 'cursorBefore']);
|
|
1353
|
+
|
|
1354
|
+
function rowMatchesQueries(row, queriesConfig) {
|
|
1355
|
+
if (!row || !Array.isArray(queriesConfig) || queriesConfig.length === 0) return true;
|
|
1356
|
+
return queriesConfig.every(query => matchQuery(row, query));
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
function matchQuery(row, query) {
|
|
1360
|
+
if (!Array.isArray(query) || query.length === 0) return true;
|
|
1361
|
+
let interpolated;
|
|
1362
|
+
try {
|
|
1363
|
+
interpolated = interpolateQuery(query);
|
|
1364
|
+
} catch (e) {
|
|
1365
|
+
if (e === NOT_READY) return true; // unresolved $auth. arg
|
|
1366
|
+
throw e;
|
|
1367
|
+
}
|
|
1368
|
+
const [method, ...args] = interpolated;
|
|
1369
|
+
if (NON_FILTER_METHODS.has(method)) return true;
|
|
1370
|
+
if (!(method in QUERY_MAP)) return true; // load drops it: unevaluable
|
|
1371
|
+
if (method === 'and') return Array.isArray(args[0]) ? args[0].every(sub => matchQuery(row, sub)) : true;
|
|
1372
|
+
if (method === 'or') return Array.isArray(args[0]) ? args[0].some(sub => matchQuery(row, sub)) : true;
|
|
1373
|
+
|
|
1374
|
+
const attr = args[0];
|
|
1375
|
+
const val = row ? row[attr] : undefined;
|
|
1376
|
+
const values = Array.isArray(args[1]) ? args[1] : [args[1]];
|
|
1377
|
+
// Strings compare case-insensitively (Appwrite's ci collation); other types strictly
|
|
1378
|
+
const eq = (a, b) => (typeof a === 'string' && typeof b === 'string')
|
|
1379
|
+
? a.toLowerCase() === b.toLowerCase()
|
|
1380
|
+
: a === b;
|
|
1381
|
+
const anyEqual = () => Array.isArray(val)
|
|
1382
|
+
? values.some(v => val.some(el => eq(el, v)))
|
|
1383
|
+
: values.some(v => eq(val, v));
|
|
1384
|
+
const cmp = (bound, op) => val !== null && val !== undefined && bound !== null && bound !== undefined && op(val, bound);
|
|
1385
|
+
const lower = s => String(s).toLowerCase();
|
|
1386
|
+
|
|
1387
|
+
switch (method) {
|
|
1388
|
+
case 'equal': return anyEqual();
|
|
1389
|
+
case 'notEqual': return !anyEqual();
|
|
1390
|
+
case 'lessThan': return cmp(args[1], (a, b) => a < b);
|
|
1391
|
+
case 'lessThanEqual': return cmp(args[1], (a, b) => a <= b);
|
|
1392
|
+
case 'greaterThan': return cmp(args[1], (a, b) => a > b);
|
|
1393
|
+
case 'greaterThanEqual': return cmp(args[1], (a, b) => a >= b);
|
|
1394
|
+
case 'between': return cmp(args[1], (a, b) => a >= b) && cmp(args[2], (a, b) => a <= b);
|
|
1395
|
+
case 'isNull': return val === null || val === undefined;
|
|
1396
|
+
case 'isNotNull': return val !== null && val !== undefined;
|
|
1397
|
+
case 'startsWith': return typeof val === 'string' && values.some(v => lower(val).startsWith(lower(v)));
|
|
1398
|
+
case 'endsWith': return typeof val === 'string' && values.some(v => lower(val).endsWith(lower(v)));
|
|
1399
|
+
case 'contains':
|
|
1400
|
+
if (Array.isArray(val)) return values.some(v => val.some(el => eq(el, v)));
|
|
1401
|
+
if (typeof val === 'string') return values.some(v => lower(val).includes(lower(v)));
|
|
1402
|
+
return false;
|
|
1403
|
+
default: return true; // search: unevaluable
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1189
1407
|
// Convert query array to Appwrite Query object
|
|
1190
1408
|
function toAppwriteQuery(queryArray) {
|
|
1191
1409
|
if (!Array.isArray(queryArray) || queryArray.length === 0) {
|
|
@@ -1194,24 +1412,9 @@ function toAppwriteQuery(queryArray) {
|
|
|
1194
1412
|
|
|
1195
1413
|
const [method, ...args] = queryArray;
|
|
1196
1414
|
|
|
1197
|
-
// Map common query methods to Appwrite Query methods
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
'notEqual': 'notEqual',
|
|
1201
|
-
'lessThan': 'lessThan',
|
|
1202
|
-
'lessThanEqual': 'lessThanEqual',
|
|
1203
|
-
'greaterThan': 'greaterThan',
|
|
1204
|
-
'greaterThanEqual': 'greaterThanEqual',
|
|
1205
|
-
'contains': 'contains',
|
|
1206
|
-
'search': 'search',
|
|
1207
|
-
'or': 'or', // Support for multi-team queries (Query.or([Query.equal(...), ...]))
|
|
1208
|
-
'orderAsc': 'orderAsc',
|
|
1209
|
-
'orderDesc': 'orderDesc',
|
|
1210
|
-
'limit': 'limit',
|
|
1211
|
-
'offset': 'offset',
|
|
1212
|
-
'cursorAfter': 'cursorAfter',
|
|
1213
|
-
'cursorBefore': 'cursorBefore'
|
|
1214
|
-
};
|
|
1415
|
+
// Map common query methods to Appwrite Query methods (QUERY_MAP above —
|
|
1416
|
+
// also the realtime matcher's evaluable set)
|
|
1417
|
+
const queryMap = QUERY_MAP;
|
|
1215
1418
|
|
|
1216
1419
|
if (!window.Appwrite || !window.Appwrite.Query) {
|
|
1217
1420
|
console.error('[Manifest Data] Appwrite Query not available');
|
|
@@ -1252,6 +1455,42 @@ function toAppwriteQuery(queryArray) {
|
|
|
1252
1455
|
}
|
|
1253
1456
|
}
|
|
1254
1457
|
|
|
1458
|
+
// Realtime-expressible operators: the intersection of what the load serializes
|
|
1459
|
+
// (QUERY_MAP), what the matcher evaluates, and what Appwrite realtime filters
|
|
1460
|
+
// server-side. isNull/isNotNull are realtime-supported but the load drops them,
|
|
1461
|
+
// so filtering on them server-side would drop events the matcher keeps — excluded.
|
|
1462
|
+
const REALTIME_FILTER_OPS = new Set(['equal', 'notEqual', 'lessThan', 'lessThanEqual', 'greaterThan', 'greaterThanEqual']);
|
|
1463
|
+
|
|
1464
|
+
function isRealtimeExpressible(query) {
|
|
1465
|
+
if (!Array.isArray(query) || query.length === 0) return false;
|
|
1466
|
+
const [method, ...args] = query;
|
|
1467
|
+
if (method === 'or') return Array.isArray(args[0]) && args[0].length > 0 && args[0].every(isRealtimeExpressible);
|
|
1468
|
+
return REALTIME_FILTER_OPS.has(method);
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
// Server-side realtime filter set, reduced to the expressible subset. Omitting
|
|
1472
|
+
// a query only WIDENS delivery — the client matcher still filters — so
|
|
1473
|
+
// unexpressible queries are dropped whole, never approximated.
|
|
1474
|
+
// mode 'scope' (default): only scope-column queries ride the subscription —
|
|
1475
|
+
// permission-mirrored, so a row leaving the filter still delivers its last
|
|
1476
|
+
// event and drops live. 'all': user queries too (a held row that stops
|
|
1477
|
+
// matching then corrects on the next load, not instantly).
|
|
1478
|
+
// Returns { queries, key } (key = canonical sorted serialization, for
|
|
1479
|
+
// subscription sharing) or null (not ready / nothing expressible → channel-only
|
|
1480
|
+
// subscribe, the 0.5.248 path).
|
|
1481
|
+
async function buildRealtimeQueries(queriesConfig, scope, scopeColumns, mode = 'scope') {
|
|
1482
|
+
const userConfig = (mode === 'all' && Array.isArray(queriesConfig)) ? queriesConfig : [];
|
|
1483
|
+
const resolved = await buildQueries(userConfig, scope, scopeColumns);
|
|
1484
|
+
if (resolved === null) return null;
|
|
1485
|
+
const queries = resolved
|
|
1486
|
+
.filter(isRealtimeExpressible)
|
|
1487
|
+
.map(query => toAppwriteQuery(query))
|
|
1488
|
+
.filter(query => query !== null);
|
|
1489
|
+
if (queries.length === 0) return null;
|
|
1490
|
+
const key = queries.map(q => typeof q === 'string' ? q : JSON.stringify(q)).sort().join('\u001e');
|
|
1491
|
+
return { queries, key };
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1255
1494
|
// Build Appwrite queries from configuration
|
|
1256
1495
|
// scopeColumns: { team, user } column names (default teamId/userId) — see manifest.data.config.js getScopeColumns
|
|
1257
1496
|
// Returns null ("not ready") straight through — see buildQueries.
|
|
@@ -1271,6 +1510,8 @@ window.ManifestDataQueries = {
|
|
|
1271
1510
|
getAuthValue,
|
|
1272
1511
|
buildQueries,
|
|
1273
1512
|
buildAppwriteQueries,
|
|
1513
|
+
buildRealtimeQueries,
|
|
1514
|
+
rowMatchesQueries,
|
|
1274
1515
|
toAppwriteQuery,
|
|
1275
1516
|
ALLOWED_VARIABLES,
|
|
1276
1517
|
DEFAULT_SCOPE_COLUMNS
|