Skip to content

Commit

Permalink
#1672 fixing the regression, moving back to listeners for the hosted …
Browse files Browse the repository at this point in the history
…mode (studio) (#1677)

Co-authored-by: kmahmood74 <[email protected]>
  • Loading branch information
kmahmood74 and kmahmood74 authored Oct 4, 2024
1 parent 5cc94ef commit 001d49c
Show file tree
Hide file tree
Showing 2 changed files with 210 additions and 132 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,14 @@ import 'package:flutter_i18n/flutter_i18n.dart';
* TODO: support initialForcedLocale
*/
class EnsembleDefinitionProvider extends DefinitionProvider {
EnsembleDefinitionProvider(this.appId, {super.initialForcedLocale}) {
appModel = AppModel(appId);
EnsembleDefinitionProvider(this.appId, {super.initialForcedLocale,this.isListenerMode = false}) {
if (isListenerMode) {
appModel = AppModelListenerMode(appId);
} else {
appModel = AppModelTimerMode(appId);
}
}

final bool isListenerMode;
final String appId;
late final AppModel appModel;

Expand Down Expand Up @@ -105,27 +109,25 @@ class EnsembleDefinitionProvider extends DefinitionProvider {
void onAppLifecycleStateChanged(AppLifecycleState state) {
// Only run the timer when the app is in the foreground
if (state == AppLifecycleState.resumed) {
appModel._startTimer();
if (appModel is AppModelTimerMode) {
(appModel as AppModelTimerMode)._startTimer();
} else {
(appModel as AppModelListenerMode).initListeners();
}
} else {
appModel._stopTimer();
if (appModel is AppModelTimerMode) {
(appModel as AppModelTimerMode)._stopTimer();
} else {
(appModel as AppModelListenerMode).cancelListeners();
}
}
}
}

class InvalidDefinition {}

class AppModel {
AppModel(this.appId) {
updateApp();
_startTimer();
}

// only 1 instance of these listeners
static StreamSubscription? _artifactListener;
static StreamSubscription? _dependentArtifactListener;

final String appId;

AppModel(this.appId);
// the cache for ID -> screen content
Map<String, dynamic> artifactCache = {};

Expand All @@ -140,102 +142,6 @@ class AppModel {
// storing the resource cache from imported apps
Map<String, dynamic> importCache = {};

/// fetch async and cache our entire App's artifacts.
/// Plus listen for changes and update the cache
String? listenerError;
/// this is latest timestamp of the updatedAt or createdAt among all artifacts
Timestamp? lastUpdatedAt;
Timestamp? internalArtifactLastUpdateAt;
/// timer to check for updates
Timer? _timer;
/// interval to check for updates
final Duration _interval = Duration(minutes: 60);
void _startTimer() {
if (_timer == null || _timer!.isActive == false) {
_timer = Timer.periodic(_interval, (timer) {
updateApp();
});
}
}

void _stopTimer() {
_timer?.cancel();
}
Future<void> updateInternalArtifacts() async {
final app = Ensemble().ensembleFirebaseApp;
final String appId = EnsembleDefinitionProvider.ensembleLibraryId;
Map<String, dynamic>? data;
bool isUpdate = internalArtifactLastUpdateAt != null;
if (isUpdate) {
print("Checking for updates of internal artifacts at: ${DateTime.now()}");
QuerySnapshot<Map<String, dynamic>> snapshot = await FirebaseFirestore.instanceFor(app: app)
.collection('apps')
.doc(appId)
.collection('artifacts')
.where('updatedAt', isGreaterThan: internalArtifactLastUpdateAt)
.get();
if (snapshot.docs.isNotEmpty) {
for (var doc in snapshot.docs) {
internalArtifactLastUpdateAt = calculateLastUpdatedAt(doc, internalArtifactLastUpdateAt);
if (doc.id == 'resources') {
data = doc.data();
}
}
print("updating internalArtifactLastUpdateAt to $internalArtifactLastUpdateAt");
}
} else {
print("first time retrieving internal artifacts at: ${DateTime.now()}");
final docSnapshot = await FirebaseFirestore.instanceFor(app: app)
.collection('apps')
.doc(appId)
.collection('artifacts')
.doc('resources')
.get();
if (docSnapshot.exists) {
data = docSnapshot.data();
internalArtifactLastUpdateAt = data?['updatedAt'];
print("updating internalArtifactLastUpdateAt to $internalArtifactLastUpdateAt");
}
}
if (data == null) {
return;
}
dynamic content = data['content'];
if (content != null) {
importCache[appId] = content;
} else {
importCache.remove(appId);
}
}
Future<void> updateApp() async {
await updateInternalArtifacts();
QuerySnapshot<Map<String, dynamic>> snapshot;
bool isUpdate = lastUpdatedAt != null;
if (isUpdate) {
print("Checking for updates at: ${DateTime.now()}");
snapshot = await _getArtifacts()
.where('isArchived', isEqualTo: false)
.where('updatedAt', isGreaterThan: lastUpdatedAt)
.get();
} else {
print("first time retrieving app at: ${DateTime.now()}");
snapshot = await _getArtifacts()
.where('isArchived', isEqualTo: false)
.get();
}
for (var doc in snapshot.docs) {
await updateArtifact(doc, isUpdate);
if (doc.data()['isRoot'] == true) {
if (doc.data()['type'] == ArtifactType.theme.name) {
themeMapping = doc.id;
} else if (doc.data()['type'] == 'screen') {
homeMapping = doc.id;
}
}
lastUpdatedAt = calculateLastUpdatedAt(doc, lastUpdatedAt);
}
}

Future<bool> updateArtifact(
DocumentSnapshot<Map<String, dynamic>> doc, bool isModified) async {
// adjust the theme and home screen
Expand Down Expand Up @@ -327,7 +233,7 @@ class AppModel {
// our listeners are not done initialized yet.
if (content == null) {
DocumentSnapshot<Map<String, dynamic>> snapshot =
await _getArtifacts().doc(screenId).get();
await _getArtifacts().doc(screenId).get();
await updateArtifact(snapshot, false);
content = artifactCache[screenId];
log("Cache missed for $screenId");
Expand Down Expand Up @@ -379,24 +285,6 @@ class AppModel {
FirebaseFirestore db = FirebaseFirestore.instanceFor(app: app);
return db.collection('apps').doc(appId).collection('artifacts');
}
Timestamp? calculateLastUpdatedAt(var artifact, Timestamp? existingLastUpdatedAt) {
// Get the 'updatedAt' or 'createdAt' timestamp
Timestamp? currentUpdatedAt;
if (artifact.data().containsKey('updatedAt') && artifact.data()['updatedAt'] != null) {
currentUpdatedAt = artifact.data()['updatedAt'] as Timestamp;
} else if (artifact.data().containsKey('createdAt') && artifact.data()['createdAt'] != null) {
currentUpdatedAt = artifact.data()['createdAt'] as Timestamp;
}

// Update the lastUpdatedAt if currentUpdatedAt is more recent
if (currentUpdatedAt != null) {
if (existingLastUpdatedAt == null || currentUpdatedAt.millisecondsSinceEpoch > existingLastUpdatedAt!.millisecondsSinceEpoch) {
print("updating lastUpdatedAt to $currentUpdatedAt");
return currentUpdatedAt;
}
}
return existingLastUpdatedAt;
}
/// App bundle for now only expects the theme, but we'll use this
/// opportunity to also cache the home page
Future<AppBundle> getAppBundle() async {
Expand Down Expand Up @@ -487,3 +375,193 @@ class AppModel {
return output;
}
}
class AppModelListenerMode extends AppModel {
AppModelListenerMode(String appId): super(appId) {
initListeners();
}
// only 1 instance of these listeners
static StreamSubscription? _artifactListener;
static StreamSubscription? _dependentArtifactListener;
/// fetch async and cache our entire App's artifacts.
/// Plus listen for changes and update the cache
String? listenerError;

void cancelListeners() async {
if (_artifactListener != null) {
await _artifactListener!.cancel();
_artifactListener = null;
}
if (_dependentArtifactListener != null) {
await _dependentArtifactListener!.cancel();
_dependentArtifactListener = null;
}
}

void initListeners() async {
final app = Ensemble().ensembleFirebaseApp;
FirebaseFirestore db = FirebaseFirestore.instanceFor(app: app);

await _artifactListener?.cancel();
_artifactListener = db
.collection('apps')
.doc(appId)
.collection('artifacts')
.where("isArchived", isEqualTo: false)
.snapshots()
.listen((event) {
for (var change in event.docChanges) {
if (change.type == DocumentChangeType.removed) {
removeArtifact(change.doc);
} else {
updateArtifact(
change.doc, change.type == DocumentChangeType.modified);
}
}
}, onError: (error) {
log("Provider listener error");
listenerError = error.toString();
});

// hardcoded to Ensemble public widget library
initWidgetArtifactListeners(EnsembleDefinitionProvider.ensembleLibraryId);
}

void initWidgetArtifactListeners(String appId) async {
final app = Ensemble().ensembleFirebaseApp;

await _dependentArtifactListener?.cancel();
_dependentArtifactListener = FirebaseFirestore.instanceFor(app: app)
.collection('apps')
.doc(appId)
.collection('artifacts')
.doc('resources')
.snapshots()
.listen((event) {
dynamic content = event.data()?['content'];
if (content != null) {
importCache[appId] = content;
} else {
importCache.remove(appId);
}
});
}
}
class AppModelTimerMode extends AppModel {
AppModelTimerMode(String appId): super(appId) {
updateApp();
_startTimer();
}

/// this is latest timestamp of the updatedAt or createdAt among all artifacts
Timestamp? lastUpdatedAt;
Timestamp? internalArtifactLastUpdateAt;
/// timer to check for updates
Timer? _timer;
/// interval to check for updates
final Duration _interval = Duration(minutes: 60);
void _startTimer() {
if (_timer == null || _timer!.isActive == false) {
_timer = Timer.periodic(_interval, (timer) {
updateApp();
});
}
}

void _stopTimer() {
_timer?.cancel();
}
Future<void> updateInternalArtifacts() async {
final app = Ensemble().ensembleFirebaseApp;
final String appId = EnsembleDefinitionProvider.ensembleLibraryId;
Map<String, dynamic>? data;
bool isUpdate = internalArtifactLastUpdateAt != null;
if (isUpdate) {
print("Checking for updates of internal artifacts at: ${DateTime.now()}");
QuerySnapshot<Map<String, dynamic>> snapshot = await FirebaseFirestore.instanceFor(app: app)
.collection('apps')
.doc(appId)
.collection('artifacts')
.where('updatedAt', isGreaterThan: internalArtifactLastUpdateAt)
.get();
if (snapshot.docs.isNotEmpty) {
for (var doc in snapshot.docs) {
internalArtifactLastUpdateAt = calculateLastUpdatedAt(doc, internalArtifactLastUpdateAt);
if (doc.id == 'resources') {
data = doc.data();
}
}
print("updating internalArtifactLastUpdateAt to $internalArtifactLastUpdateAt");
}
} else {
print("first time retrieving internal artifacts at: ${DateTime.now()}");
final docSnapshot = await FirebaseFirestore.instanceFor(app: app)
.collection('apps')
.doc(appId)
.collection('artifacts')
.doc('resources')
.get();
if (docSnapshot.exists) {
data = docSnapshot.data();
internalArtifactLastUpdateAt = data?['updatedAt'];
print("updating internalArtifactLastUpdateAt to $internalArtifactLastUpdateAt");
}
}
if (data == null) {
return;
}
dynamic content = data['content'];
if (content != null) {
importCache[appId] = content;
} else {
importCache.remove(appId);
}
}
Future<void> updateApp() async {
await updateInternalArtifacts();
QuerySnapshot<Map<String, dynamic>> snapshot;
bool isUpdate = lastUpdatedAt != null;
if (isUpdate) {
print("Checking for updates at: ${DateTime.now()}");
snapshot = await _getArtifacts()
.where('isArchived', isEqualTo: false)
.where('updatedAt', isGreaterThan: lastUpdatedAt)
.get();
} else {
print("first time retrieving app at: ${DateTime.now()}");
snapshot = await _getArtifacts()
.where('isArchived', isEqualTo: false)
.get();
}
for (var doc in snapshot.docs) {
await updateArtifact(doc, isUpdate);
if (doc.data()['isRoot'] == true) {
if (doc.data()['type'] == ArtifactType.theme.name) {
themeMapping = doc.id;
} else if (doc.data()['type'] == 'screen') {
homeMapping = doc.id;
}
}
lastUpdatedAt = calculateLastUpdatedAt(doc, lastUpdatedAt);
}
}

Timestamp? calculateLastUpdatedAt(var artifact, Timestamp? existingLastUpdatedAt) {
// Get the 'updatedAt' or 'createdAt' timestamp
Timestamp? currentUpdatedAt;
if (artifact.data().containsKey('updatedAt') && artifact.data()['updatedAt'] != null) {
currentUpdatedAt = artifact.data()['updatedAt'] as Timestamp;
} else if (artifact.data().containsKey('createdAt') && artifact.data()['createdAt'] != null) {
currentUpdatedAt = artifact.data()['createdAt'] as Timestamp;
}

// Update the lastUpdatedAt if currentUpdatedAt is more recent
if (currentUpdatedAt != null) {
if (existingLastUpdatedAt == null || currentUpdatedAt.millisecondsSinceEpoch > existingLastUpdatedAt!.millisecondsSinceEpoch) {
print("updating lastUpdatedAt to $currentUpdatedAt");
return currentUpdatedAt;
}
}
return existingLastUpdatedAt;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import 'package:yaml/yaml.dart';

/// Connecting to Ensemble-hosted definitions with a host persistent cache
class HostCachedEnsembleProvider extends EnsembleDefinitionProvider {
HostCachedEnsembleProvider._create(String appId) : super(appId);
HostCachedEnsembleProvider._create(String appId) : super(appId,isListenerMode: true);

late SharedPreferences hostCache;

Expand Down

0 comments on commit 001d49c

Please sign in to comment.