{"id":18563119,"url":"https://github.com/compulim/bookstore","last_synced_at":"2025-05-15T18:12:48.354Z","repository":{"id":82478802,"uuid":"125486139","full_name":"compulim/bookstore","owner":"compulim","description":"A data storage pattern for Azure Blob Storage","archived":false,"fork":false,"pushed_at":"2019-03-26T03:29:00.000Z","size":489,"stargazers_count":2,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-04-21T07:42:37.204Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/compulim.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2018-03-16T08:24:22.000Z","updated_at":"2019-03-26T03:28:58.000Z","dependencies_parsed_at":"2023-07-16T05:16:44.393Z","dependency_job_id":null,"html_url":"https://github.com/compulim/bookstore","commit_stats":{"total_commits":41,"total_committers":1,"mean_commits":41.0,"dds":0.0,"last_synced_commit":"c9d2b84f6dfb2b35b74fb699e7f9218f76102521"},"previous_names":[],"tags_count":3,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/compulim%2Fbookstore","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/compulim%2Fbookstore/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/compulim%2Fbookstore/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/compulim%2Fbookstore/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/compulim","download_url":"https://codeload.github.com/compulim/bookstore/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":254394726,"owners_count":22063984,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-11-06T22:11:51.766Z","updated_at":"2025-05-15T18:12:45.832Z","avatar_url":"https://github.com/compulim.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"```\n _                 _        _\n| |__   ___   ___ | | _____| |_ ___  _ __ ___\n| '_ \\ / _ \\ / _ \\| |/ / __| __/ _ \\| '__/ _ \\\n| |_) | (_) | (_) |   \u003c\\__ \\ || (_) | | |  __/\n|_.__/ \\___/ \\___/|_|\\_\\___/\\__\\___/|_|  \\___|\n\n```\n\n\u003c!-- The ASCII art is generated using http://www.patorjk.com/software/taag/#p=display\u0026f=Ogre\u0026t=bookstore --\u003e\n\nA small data framework for collaborative list editing using Azure Blob Storage and Redis.\n\n\u003ca href=\"https://badge.fury.io/js/bookstore\"\u003e\u003cimg alt=\"npm version\" src=\"https://badge.fury.io/js/bookstore.svg\" /\u003e\u003c/a\u003e\n\u003ca href=\"https://travis-ci.org/compulim/bookstore\"\u003e\u003cimg alt=\"Build Status\" src=\"https://travis-ci.org/compulim/bookstore.svg?branch=master\" /\u003e\u003c/a\u003e\n\u003ca href=\"https://coveralls.io/github/compulim/bookstore?branch=master\"\u003e\u003cimg src=\"https://coveralls.io/repos/github/compulim/bookstore/badge.svg?branch=master\" alt=\"Coverage Status\" /\u003e\u003c/a\u003e\n\n# What is different than traditional CRUD + cache?\n\n- Content and summary\n   - Summary is computed from content, via a summarizer function\n   - When content is updated, only summary is broadcasted via Redis to other nodes\n   - List will fetch all summaries, without content\n   - List is cheap, `O(1)`\n- Update is done thru lock-update-unlock pattern with updater function\n   - Updater function can be programmed as optimistic or pessimistic concurrency\n   - We believe this model makes concurrency issues a little bit easier to handle\n\n`bookstore` is designed to be exposed over Web Socket and Server-Sent Events.\n\n# How to use\n\nFor production build, `npm install bookstore`. For development build, `npm install bookstore@master`.\n\nFor peer dependencies, you will also need `npm install azure-storage@2 redis`.\n\n```js\nconst { createBlobService } = require('azure-storage');\nconst { createClient } = require('redis');\nconst updateIn = require('simple-update-in');\nconst createBook, { createPubSubUsingRedis, createStorageUsingAzureStorage } = require('bookstore');\n\nconst blobService = createBlobService(\n  process.env.AZURE_STORAGE_ACCOUNT,\n  process.env.AZURE_STORAGE_ACCESS_KEY\n);\n\nconst publishRedis = createClient();\nconst subscribeRedis = publishRedis.duplicate();\n\nconst book = createBook(\n  ({ x, y }) =\u003e ({ sum: x + y }),\n  {\n    ...createPubSubUsingRedis(publishRedis, subscribeRedis, 'blob-container-name'),\n    ...createStorageUsingAzureStorage(blobService, 'blob-container-name')\n  }\n);\n\nawait book.create('page-0', { x: 1, y: 2 });\n\n// Listing summary of all pages\n// { 'page-0': {\n//   summary: { sum: 3 }\n// } }\nawait book.list();\n\n// Getting the content of the page\n// { x: 1, y: 2 }\nawait book.get('page-0');\n\n// Update a page\n// We use `simple-update-in` and updater function for handling concurrency\nawait book.update('page-0', content =\u003e updateIn(content, ['x'], () =\u003e 3));\n\n// \"change\" event emitted when update broadcast on Redis\n// Only summaries are sent over Redis\nbook.subscribe(({ id, summary }) =\u003e {\n  console.log(id); // 'page-0'\n  console.log(summary); // { sum: 5 }\n});\n\n// Listing summary of all pages again, with new changes\n// { 'page-0': {\n//   summary: { sum: 5 }\n// } }\nawait book.list();\n\n// Delete a page\nawait book.del('page-0');\n```\n\n## Peer requirements\n\nInstead of using Blob via Azure Storage and Pub-sub via Redis, you can also use other services as long as they met the requirements:\n\n- Storage\n   - `create(id, content, summary)`: Create a new blob with content and summary\n   - `del(id)`: Delete a blob\n   - `get(id)`: Read a blob content\n   - `list()`: List all blob summaries, without reading the actual content\n   - `update(id, updater)`: Update an existing blob via an updater function, using lock to prevent dirty read\n      - `updater: ({ content, summary }) =\u003e ({ content, summary })`\n- Pub-sub\n   - `publish(content)`: Publish to a predefined topic\n   - `subscribe(callback: content =\u003e void): () =\u003e void`: Subscribe to a predefined topic via callback, will return a function for unsubscribe\n      - `callback` will be called if content is updated (via `Object.is`) and summary has kept the same\n\n# Contributions\n\nLike us? [Star](https://github.com/compulim/bookstore/stargazers) us.\n\nWant to make it better? [File](https://github.com/compulim/bookstore/issues) us an issue.\n\nDon't like something you see? [Submit](https://github.com/compulim/bookstore/pulls) a pull request.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcompulim%2Fbookstore","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcompulim%2Fbookstore","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcompulim%2Fbookstore/lists"}