altseven Service component - experimental features

I have been working on expanding the altseven JS framework to simplify retrieving and caching data from the server side of an application. To this end, I have created a services module and a Service component that you can use to load and cache data from the server. Each Service component handles a set of data exchange methods with the server, including basic CRUD operations. At the moment, it also has standard readAll and readMany methods. Note that these methods are not implementations of remote methods, they link to methods in the a7.remote module.

a7.components.Service

function Service(props) {
	this.id = props.id; // id of the srevice to register with the framework
	this.key = props.key; // name of the Object key
	this.remoteMethods = props.remoteMethods;
	this.entity = props.entity;
	this.config();
	this.fireEvent("mustRegister");
}

Service.prototype = {
	config: function () {
		let dataMap = a7.model.get(this.id);
		if (!dataMap || !(dataMap instanceof Map)) {
			a7.model.set(this.id, new Map());
		}

		this.on(
			"mustRegister",
			function () {
				a7.log.trace("mustRegister: Service: " + this.id);
				a7.services.register(this);
			}.bind(this),
		);
	},
	events: ["mustRegister", "registered", "mustPurgeCache", "cachePurged"],
	convertArrayToMap: function (dataArray) {
		let dataMap = new Map();
		dataArray.forEach((item) => {
			if (item[this.key]) {
				dataMap.set(item[this.key], item);
			}
		});
		//this.mergeItems(itemArray);
		return dataMap;
	},

	convertMapToArray: function (dataMap) {
		let dataArray = [];
		dataMap.forEach((item) => {
			dataArray.push(item);
		});
		return dataArray;
	},
	// Compare itemIDs against cached items
	compareIDs: function (IDs) {
		const dataMap = a7.model.get(this.id);
		const present = [];
		const missing = [];

		if (!(dataMap instanceof Map)) {
			return { present, missing: IDs };
		}

		IDs.forEach((id) => {
			if (dataMap.has(id)) {
				present.push(id);
			} else {
				missing.push(id);
			}
		});

		return { present, missing };
	},

	// Merge new items into the existing Map
	mergeItems: function (newItems) {
		let dataMap = a7.model.get(this.id);

		if (!(dataMap instanceof Map)) {
			dataMap = new Map();
		}

		newItems.forEach((item) => {
			if (item[this.key]) {
				dataMap.set(item[this.key], item);
			}
		});

		a7.model.set(this.id, dataMap);
		return dataMap;
	},

	new: function () {
		return Object.assign({}, this.entity);
	},

	create: async function (obj) {
		let entity = obj;
		await a7.remote
			.invoke(this.remoteMethods.create, obj)
			.then(function (response) {
				return response.json();
			})
			.then((json) => {
				this.cacheSet(json);
				entity = json;
			});
		return entity;
	},

	read: async function (obj) {
		let dataMap = a7.model.get(this.id);
		if (!dataMap.has(this.id)) {
			await a7.remote
				.invoke(this.remoteMethods.read, obj)
				.then(function (response) {
					return response.json();
				})
				.then((json) => {
					this.cacheSet(json);
				});
		}
		return dataMap.get(this.id);
	},

	update: async function (obj) {
		let entity = obj;
		await a7.remote
			.invoke(this.remoteMethods.update, obj)
			.then(function (response) {
				return response.json();
			})
			.then((json) => {
				this.cacheSet(json);
				obj = json;
			});
		return entity;
	},

	delete: async function (obj) {
		await a7.remote
			.invoke(this.remoteMethods.delete, obj)
			.then(function (response) {
				return response.json();
			})
			.then((json) => {
				this.cacheDelete(obj[this.key]);
			});
		return true;
	},

	readAll: async function (obj) {
		let dataMap = a7.model.get(this.id);
		if (!dataMap.size) {
			await a7.remote
				.invoke(this.remoteMethods.readAll, obj)
				.then(function (response) {
					return response.json();
				})
				.then((json) => {
					this.mergeItems(json);
				});
		}
		return a7.model.get(this.id);
	},

	cacheDelete: function (id) {
		let dataMap = a7.model.get(this.id);
		dataMap.delete(id);
		a7.model.set(this.id, dataMap);
	},

	cacheSet: function (item) {
		let dataMap = a7.model.get(this.id);
		dataMap.set(item[this.key], item);
		a7.model.set(this.id, dataMap);
	},

	// Retrieve items, using cache when possible
	readMany: async function (IDs) {
		// Compare requested IDs with cache
		const { present, missing } = this.compareItemIDs(IDs);

		// Fetch missing items if any

		if (missing.length > 0) {
			let obj = { id: missing };

			await a7.remote
				.invoke(this.remoteMethods.readMany, obj)
				//.fetch("/api/items/" + missing.toString(), params, true)
				.then(function (response) {
					return response.json();
				})
				.then((json) => {
					if (Array.isArray(json)) {
						this.mergeItems(json);
					}
				});
		}

		// Get cached items
		const itemsMap = a7.model.get(this.key);
		const cachedItems = present.map((id) => itemsMap.get(id));

		// Return all requested items in order, filtering out nulls
		const result = IDs.map((id) => {
			const item = itemsMap.get(id);
			return item || null; // Return null for items that couldn't be found
		});
		return result.filter((item) => item !== null); // Filter out any null values
	},
};

a7.services

a7.services = (function () {
	"use strict";

	const _services = new Map();

	return {
		init: function (options) {
			// init the services module
			// add services
			for (let service in options.services) {
				a7.services.setService(service);
			}
		},

		getService: function (id) {
			return _services.get(id);
		},

		register: function (service) {
			_services.set(service.id, service);
		},
	};
})();

Here is an implementation of the Service component for bookmarks. By using the Constructor component to invoke the component, we are able to leverage the event bindings in altseven, as are used in the View component. That means We can add listeners to any invoked component for the events listed in the Service component and act on those events. Note that this is currently experimental code and things may change significantly.

import { a7 } from "/lib/altseven/dist/a7.js";

export var Bookmark = function Bookmark() {
	const props = {
		id: "bookmarkService",
		remoteMethods: {
			create: "bookmark.create",
			read: "bookmark.read",
			update: "bookmark.update",
			delete: "bookmark.deleteById",
			readAll: "bookmark.getAll",
		},
		key: "bookmarkID",
		entity: {
			bookmarkID: "",
			postID: "",
			bookmarkFolderID: "",
		},
	};

	var post = a7.components.Constructor(a7.components.Service, [props], true);

	return post;
};

Right now, all you need to do is import the service and call it after the a7.init() function is called.

import { Bookmark } from "/assets/js/service/bookmark.js";

Bookmark();

Calling it registers the component with the a7.services module, making it possible to call and use the component from anywhere in the application like so:

		let bookmarkService = a7.services.getService("bookmarkService");
		let bmarks = await bookmarkService.readAll();

As you can see above, the Service component caches retrieved data using the a7.model module, meaning that previously retrieved data does not need to be retrieved over and over again. Updating and deleting records changes the cache. Right now, I am looking at adding filtering to the service so you can filter retrieved data and return a subset of what is there.

Next Up

Next, I am working on implementing dataProviders for the View component, where a view’s state can be linked to a key in the a7.model cache. Using this system, it should be possible to retrive, update, and delete data and have that data automatically update View components in altseven. The goal is to simplify the process of managing and updating Views. There are some inherent complexities in this process, but the goal is to automate away as much manual work as possible.