mnfst 0.5.246 → 0.5.248

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.246';
5
+ const MANIFEST_BUILD_VERSION = '0.5.248';
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: window.Appwrite.Realtime ? new window.Appwrite.Realtime(client) : null
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, () => { authCheck = null; authVerified = false; }));
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,103 +682,89 @@ window.ManifestDataAppwrite = {
665
682
  };
666
683
 
667
684
 
668
- /* Manifest Data Sources - Real-time Subscriptions */
669
-
670
- // Track active subscriptions
671
- const subscriptions = new Map(); // Map<dataSourceName, unsubscribeFunction>
685
+ /* Manifest Data Sources - Real-time Subscriptions
686
+ One shared Realtime client per tab; channels ref-counted across sources. */
687
+
688
+ // Map<channel, { refs, unsubscribe, handlers: Map<sourceName, handler> }>
689
+ const channels = new Map();
690
+ // Map<sourceName, { channel, token }> — token guards a stale unsubscribe closure
691
+ const sources = new Map();
692
+ // Map<sourceName, fn(eventType, row)> — pre-filters, run per event per source
693
+ const filters = new Map();
694
+ // Back-compat/debug surface: Map<sourceName, unsubscribeFunction>
695
+ const subscriptions = new Map();
696
+ // Instance the current channel subscriptions live on
697
+ let activeRealtime = null;
698
+
699
+ function classify(event) {
700
+ if (typeof event !== 'string') return null;
701
+ if (event.includes('create')) return 'create';
702
+ if (event.includes('update')) return 'update';
703
+ if (event.includes('delete')) return 'delete';
704
+ return null;
705
+ }
672
706
 
673
- // Subscribe to storage bucket file changes
674
- // isCurrent: false once the requesting load is superseded (logout) — skip, never touch a newer entry
675
- async function subscribeToStorageBucket(dataSourceName, bucketId, scope, onEvent, isCurrent) {
676
- // Unsubscribe from existing subscription if any
677
- if (subscriptions.has(dataSourceName)) {
678
- const unsubscribe = subscriptions.get(dataSourceName);
679
- if (unsubscribe && typeof unsubscribe === 'function') {
680
- unsubscribe();
707
+ // Fan one SDK message out: each registered source handler gets each event once,
708
+ // after that source's pre-filter (setFilter) — before any merge/dedup work
709
+ function dispatch(channel, response) {
710
+ const entry = channels.get(channel);
711
+ if (!entry || !response || !response.events) return;
712
+ const events = Array.isArray(response.events) ? response.events : [response.events];
713
+ let payload = response.payload;
714
+ if (!payload || (!payload.$id && !payload.row)) payload = response;
715
+ for (const event of events) {
716
+ const type = classify(event);
717
+ if (!type) continue;
718
+ for (const [name, handler] of entry.handlers) {
719
+ const filter = filters.get(name);
720
+ if (filter && filter(type, payload) === false) continue;
721
+ handler(type, payload);
681
722
  }
682
- subscriptions.delete(dataSourceName);
683
723
  }
724
+ }
684
725
 
685
- const services = await window.ManifestDataAppwrite.getAppwriteDataServices();
686
- if (isCurrent && !isCurrent()) return null;
687
- if (!services?.realtime) {
688
- console.warn('[Manifest Data] Realtime service not available for', dataSourceName);
689
- return null;
690
- }
691
-
692
- // Channel format: buckets.[BUCKET_ID].files
693
- const channel = `buckets.${bucketId}.files`;
694
-
695
-
696
- try {
697
- const unsubscribe = services.realtime.subscribe(channel, (response) => {
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;
728
- } catch (error) {
729
- console.error('[Manifest Data] Failed to subscribe to storage bucket:', error);
730
- return null;
731
- }
726
+ function sdkSubscribe(realtime, channel) {
727
+ return realtime.subscribe(channel, response => dispatch(channel, response));
732
728
  }
733
729
 
734
- // Unsubscribe from a data source
735
- function unsubscribeFromDataSource(dataSourceName) {
736
- if (subscriptions.has(dataSourceName)) {
737
- const unsubscribe = subscriptions.get(dataSourceName);
738
- if (unsubscribe && typeof unsubscribe === 'function') {
739
- unsubscribe();
730
+ // Drop a source's registration; tear the channel down when the last source leaves
731
+ function detach(name, token) {
732
+ const cur = sources.get(name);
733
+ if (!cur || (token && cur.token !== token)) return;
734
+ sources.delete(name);
735
+ const entry = channels.get(cur.channel);
736
+ if (!entry) return;
737
+ entry.handlers.delete(name);
738
+ entry.refs = entry.handlers.size;
739
+ if (entry.refs === 0) {
740
+ channels.delete(cur.channel);
741
+ if (typeof entry.unsubscribe === 'function') {
742
+ try { entry.unsubscribe(); } catch (e) { /* socket already gone */ }
740
743
  }
741
- subscriptions.delete(dataSourceName);
742
744
  }
743
745
  }
744
746
 
745
- // Unsubscribe from all data sources
746
- function unsubscribeAll() {
747
- subscriptions.forEach((unsubscribe, dataSourceName) => {
748
- if (unsubscribe && typeof unsubscribe === 'function') {
749
- unsubscribe();
747
+ // Instance changed (client rebuilt on identity change): re-bind live channels
748
+ function syncInstance(realtime) {
749
+ if (activeRealtime === realtime) return;
750
+ activeRealtime = realtime;
751
+ for (const [channel, entry] of channels) {
752
+ if (typeof entry.unsubscribe === 'function') {
753
+ try { entry.unsubscribe(); } catch (e) { /* old instance */ }
750
754
  }
751
- });
752
- subscriptions.clear();
753
- }
754
-
755
- // Subscribe to database table row changes
756
- async function subscribeToTable(dataSourceName, databaseId, tableId, scope, onEvent, isCurrent) {
757
- // Unsubscribe from existing subscription if any
758
- if (subscriptions.has(dataSourceName)) {
759
- const unsubscribe = subscriptions.get(dataSourceName);
760
- if (unsubscribe && typeof unsubscribe === 'function') {
761
- unsubscribe();
755
+ try {
756
+ entry.unsubscribe = sdkSubscribe(realtime, channel);
757
+ } catch (error) {
758
+ console.error('[Manifest Data] Failed to re-subscribe channel:', channel, error);
762
759
  }
763
- subscriptions.delete(dataSourceName);
764
760
  }
761
+ }
762
+
763
+ // One SDK subscribe per distinct channel, however many sources share it.
764
+ // isCurrent: false once the requesting load is superseded (logout) — skip, never touch a newer entry
765
+ async function subscribeChannel(dataSourceName, channel, onEvent, isCurrent) {
766
+ detach(dataSourceName);
767
+ subscriptions.delete(dataSourceName);
765
768
 
766
769
  const services = await window.ManifestDataAppwrite.getAppwriteDataServices();
767
770
  if (isCurrent && !isCurrent()) return null;
@@ -769,55 +772,67 @@ async function subscribeToTable(dataSourceName, databaseId, tableId, scope, onEv
769
772
  console.warn('[Manifest Data] Realtime service not available for', dataSourceName);
770
773
  return null;
771
774
  }
775
+ syncInstance(services.realtime);
772
776
 
773
- // Channel format: databases.[DATABASE_ID].tables.[TABLE_ID].rows
774
- const channel = `databases.${databaseId}.tables.${tableId}.rows`;
775
-
776
- try {
777
- const unsubscribe = services.realtime.subscribe(channel, (response) => {
778
- if (!response || !response.events) {
779
- return;
780
- }
777
+ let entry = channels.get(channel);
778
+ if (!entry) {
779
+ entry = { refs: 0, unsubscribe: null, handlers: new Map() };
780
+ try {
781
+ entry.unsubscribe = sdkSubscribe(services.realtime, channel);
782
+ } catch (error) {
783
+ console.error('[Manifest Data] Failed to subscribe:', channel, error);
784
+ return null;
785
+ }
786
+ channels.set(channel, entry);
787
+ }
788
+ const token = Symbol(dataSourceName);
789
+ entry.handlers.set(dataSourceName, onEvent);
790
+ entry.refs = entry.handlers.size;
791
+ sources.set(dataSourceName, { channel, token });
781
792
 
782
- // Handle both array and single event formats
783
- const events = Array.isArray(response.events) ? response.events : [response.events];
793
+ const unsubscribe = () => detach(dataSourceName, token);
794
+ subscriptions.set(dataSourceName, unsubscribe);
795
+ return unsubscribe;
796
+ }
784
797
 
785
- events.forEach(event => {
786
- if (typeof event !== 'string') {
787
- return;
788
- }
798
+ // Subscribe to storage bucket file changes
799
+ function subscribeToStorageBucket(dataSourceName, bucketId, scope, onEvent, isCurrent) {
800
+ return subscribeChannel(dataSourceName, `buckets.${bucketId}.files`, onEvent, isCurrent);
801
+ }
789
802
 
790
- // Payload might be in response.payload or response directly
791
- // Appwrite realtime structure: { events: [...], payload: {...} }
792
- let payload = response.payload;
803
+ // Subscribe to database table row changes
804
+ function subscribeToTable(dataSourceName, databaseId, tableId, scope, onEvent, isCurrent) {
805
+ return subscribeChannel(dataSourceName, `databases.${databaseId}.tables.${tableId}.rows`, onEvent, isCurrent);
806
+ }
793
807
 
794
- // If payload doesn't have $id, try response directly
795
- if (!payload || (!payload.$id && !payload.row)) {
796
- payload = response;
797
- }
808
+ // Unsubscribe from a data source
809
+ function unsubscribeFromDataSource(dataSourceName) {
810
+ detach(dataSourceName);
811
+ subscriptions.delete(dataSourceName);
812
+ }
798
813
 
799
- // Handle different event types
800
- if (event.includes('create') || event.includes('rows.create') || event.includes('documents.create')) {
801
- // New row created
802
- onEvent('create', payload);
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
- });
814
+ // Unsubscribe from all data sources
815
+ function unsubscribeAll() {
816
+ for (const name of [...sources.keys()]) detach(name);
817
+ subscriptions.clear();
818
+ }
813
819
 
814
- subscriptions.set(dataSourceName, unsubscribe);
820
+ // Programmatic pre-filter: fn(eventType, row) runs per event per source BEFORE
821
+ // any merge/dedup work; returning false drops the event. null clears it.
822
+ function setFilter(sourceName, fn) {
823
+ if (fn == null) filters.delete(sourceName);
824
+ else if (typeof fn === 'function') filters.set(sourceName, fn);
825
+ }
815
826
 
816
- return unsubscribe;
817
- } catch (error) {
818
- console.error('[Manifest Data] Failed to subscribe to table:', error);
819
- return null;
820
- }
827
+ // Identity events rebuild the shared Realtime (manifest.data.appwrite.js drops
828
+ // its memo on the same events); re-bind any live channels to the new instance
829
+ if (typeof window !== 'undefined') {
830
+ ['manifest:auth:login', 'manifest:auth:logout', 'manifest:auth:anonymous', 'manifest:auth:session-cleared', 'manifest:auth:initialized']
831
+ .forEach(type => window.addEventListener(type, async () => {
832
+ if (!channels.size) return;
833
+ const services = await window.ManifestDataAppwrite?.getAppwriteDataServices?.();
834
+ if (services?.realtime) syncInstance(services.realtime);
835
+ }));
821
836
  }
822
837
 
823
838
  // Export functions
@@ -826,7 +841,9 @@ window.ManifestDataRealtime = {
826
841
  subscribeToTable,
827
842
  unsubscribeFromDataSource,
828
843
  unsubscribeAll,
829
- subscriptions // Expose for debugging
844
+ setFilter,
845
+ subscriptions, // Expose for debugging
846
+ _channels: channels // Expose for debugging
830
847
  };
831
848
 
832
849
 
@@ -1186,6 +1203,86 @@ async function buildQueries(queriesConfig, scope, scopeColumns) {
1186
1203
  return [...scopeQueries, ...userQueries];
1187
1204
  }
1188
1205
 
1206
+ // Query methods buildAppwriteQueries serializes (anything else is dropped at
1207
+ // load and must therefore never drop rows in the realtime matcher below)
1208
+ const QUERY_MAP = {
1209
+ 'equal': 'equal',
1210
+ 'notEqual': 'notEqual',
1211
+ 'lessThan': 'lessThan',
1212
+ 'lessThanEqual': 'lessThanEqual',
1213
+ 'greaterThan': 'greaterThan',
1214
+ 'greaterThanEqual': 'greaterThanEqual',
1215
+ 'contains': 'contains',
1216
+ 'search': 'search',
1217
+ 'or': 'or', // Support for multi-team queries (Query.or([Query.equal(...), ...]))
1218
+ 'orderAsc': 'orderAsc',
1219
+ 'orderDesc': 'orderDesc',
1220
+ 'limit': 'limit',
1221
+ 'offset': 'offset',
1222
+ 'cursorAfter': 'cursorAfter',
1223
+ 'cursorBefore': 'cursorBefore'
1224
+ };
1225
+
1226
+ // Client-side row matcher for realtime events: evaluates a source's configured
1227
+ // filter queries against one row. Only operators the load actually serializes
1228
+ // (QUERY_MAP) are evaluable. Liveness over precision: anything unevaluable —
1229
+ // search, an operator the load drops, or a $auth. arg that can't resolve
1230
+ // synchronously — counts as a MATCH so live events are never silently dropped.
1231
+ const NON_FILTER_METHODS = new Set(['limit', 'offset', 'orderAsc', 'orderDesc', 'select', 'cursorAfter', 'cursorBefore']);
1232
+
1233
+ function rowMatchesQueries(row, queriesConfig) {
1234
+ if (!row || !Array.isArray(queriesConfig) || queriesConfig.length === 0) return true;
1235
+ return queriesConfig.every(query => matchQuery(row, query));
1236
+ }
1237
+
1238
+ function matchQuery(row, query) {
1239
+ if (!Array.isArray(query) || query.length === 0) return true;
1240
+ let interpolated;
1241
+ try {
1242
+ interpolated = interpolateQuery(query);
1243
+ } catch (e) {
1244
+ if (e === NOT_READY) return true; // unresolved $auth. arg
1245
+ throw e;
1246
+ }
1247
+ const [method, ...args] = interpolated;
1248
+ if (NON_FILTER_METHODS.has(method)) return true;
1249
+ if (!(method in QUERY_MAP)) return true; // load drops it: unevaluable
1250
+ if (method === 'and') return Array.isArray(args[0]) ? args[0].every(sub => matchQuery(row, sub)) : true;
1251
+ if (method === 'or') return Array.isArray(args[0]) ? args[0].some(sub => matchQuery(row, sub)) : true;
1252
+
1253
+ const attr = args[0];
1254
+ const val = row ? row[attr] : undefined;
1255
+ const values = Array.isArray(args[1]) ? args[1] : [args[1]];
1256
+ // Strings compare case-insensitively (Appwrite's ci collation); other types strictly
1257
+ const eq = (a, b) => (typeof a === 'string' && typeof b === 'string')
1258
+ ? a.toLowerCase() === b.toLowerCase()
1259
+ : a === b;
1260
+ const anyEqual = () => Array.isArray(val)
1261
+ ? values.some(v => val.some(el => eq(el, v)))
1262
+ : values.some(v => eq(val, v));
1263
+ const cmp = (bound, op) => val !== null && val !== undefined && bound !== null && bound !== undefined && op(val, bound);
1264
+ const lower = s => String(s).toLowerCase();
1265
+
1266
+ switch (method) {
1267
+ case 'equal': return anyEqual();
1268
+ case 'notEqual': return !anyEqual();
1269
+ case 'lessThan': return cmp(args[1], (a, b) => a < b);
1270
+ case 'lessThanEqual': return cmp(args[1], (a, b) => a <= b);
1271
+ case 'greaterThan': return cmp(args[1], (a, b) => a > b);
1272
+ case 'greaterThanEqual': return cmp(args[1], (a, b) => a >= b);
1273
+ case 'between': return cmp(args[1], (a, b) => a >= b) && cmp(args[2], (a, b) => a <= b);
1274
+ case 'isNull': return val === null || val === undefined;
1275
+ case 'isNotNull': return val !== null && val !== undefined;
1276
+ case 'startsWith': return typeof val === 'string' && values.some(v => lower(val).startsWith(lower(v)));
1277
+ case 'endsWith': return typeof val === 'string' && values.some(v => lower(val).endsWith(lower(v)));
1278
+ case 'contains':
1279
+ if (Array.isArray(val)) return values.some(v => val.some(el => eq(el, v)));
1280
+ if (typeof val === 'string') return values.some(v => lower(val).includes(lower(v)));
1281
+ return false;
1282
+ default: return true; // search: unevaluable
1283
+ }
1284
+ }
1285
+
1189
1286
  // Convert query array to Appwrite Query object
1190
1287
  function toAppwriteQuery(queryArray) {
1191
1288
  if (!Array.isArray(queryArray) || queryArray.length === 0) {
@@ -1194,24 +1291,9 @@ function toAppwriteQuery(queryArray) {
1194
1291
 
1195
1292
  const [method, ...args] = queryArray;
1196
1293
 
1197
- // Map common query methods to Appwrite Query methods
1198
- const queryMap = {
1199
- 'equal': 'equal',
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
- };
1294
+ // Map common query methods to Appwrite Query methods (QUERY_MAP above —
1295
+ // also the realtime matcher's evaluable set)
1296
+ const queryMap = QUERY_MAP;
1215
1297
 
1216
1298
  if (!window.Appwrite || !window.Appwrite.Query) {
1217
1299
  console.error('[Manifest Data] Appwrite Query not available');
@@ -1271,6 +1353,7 @@ window.ManifestDataQueries = {
1271
1353
  getAuthValue,
1272
1354
  buildQueries,
1273
1355
  buildAppwriteQueries,
1356
+ rowMatchesQueries,
1274
1357
  toAppwriteQuery,
1275
1358
  ALLOWED_VARIABLES,
1276
1359
  DEFAULT_SCOPE_COLUMNS