@xyo-network/archivist-indexeddb 2.87.1 → 2.88.0

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.
@@ -7,7 +7,6 @@ var __publicField = (obj, key, value) => {
7
7
  };
8
8
 
9
9
  // src/Archivist.ts
10
- import { assertEx } from "@xylabs/assert";
11
10
  import { exists } from "@xylabs/exists";
12
11
  import { AbstractArchivist } from "@xyo-network/archivist-abstract";
13
12
  import { ArchivistAllQuerySchema, ArchivistClearQuerySchema, ArchivistDeleteQuerySchema, ArchivistInsertQuerySchema, buildStandardIndexName } from "@xyo-network/archivist-model";
@@ -34,7 +33,6 @@ function _ts_decorate(decorators, target, key, desc) {
34
33
  }
35
34
  __name(_ts_decorate, "_ts_decorate");
36
35
  var _IndexedDbArchivist = class _IndexedDbArchivist extends AbstractArchivist {
37
- _db;
38
36
  /**
39
37
  * The database name. If not supplied via config, it defaults
40
38
  * to the module name (not guaranteed to be unique) and if module
@@ -70,76 +68,108 @@ var _IndexedDbArchivist = class _IndexedDbArchivist extends AbstractArchivist {
70
68
  var _a;
71
69
  return ((_a = this.config) == null ? void 0 : _a.storeName) ?? _IndexedDbArchivist.defaultStoreName;
72
70
  }
73
- get db() {
74
- return assertEx(this._db, "DB not initialized");
75
- }
71
+ /**
72
+ * The indexes to create on the store
73
+ */
76
74
  get indexes() {
77
75
  var _a, _b;
78
- return ((_b = (_a = this.config) == null ? void 0 : _a.storage) == null ? void 0 : _b.indexes) ?? [];
76
+ return [
77
+ _IndexedDbArchivist.hashIndex,
78
+ _IndexedDbArchivist.schemaIndex,
79
+ ...((_b = (_a = this.config) == null ? void 0 : _a.storage) == null ? void 0 : _b.indexes) ?? []
80
+ ];
79
81
  }
80
82
  async allHandler() {
81
- const payloads = await this.db.getAll(this.storeName);
83
+ const payloads = await this.useDb((db) => db.getAll(this.storeName));
82
84
  return payloads.map((payload) => PayloadHasher.jsonPayload(payload));
83
85
  }
84
86
  async clearHandler() {
85
- await this.db.clear(this.storeName);
87
+ await this.useDb((db) => db.clear(this.storeName));
86
88
  }
87
89
  async deleteHandler(hashes) {
88
90
  const distinctHashes = [
89
91
  ...new Set(hashes)
90
92
  ];
91
- const found = await Promise.all(distinctHashes.map(async (hash) => {
92
- let existing;
93
- do {
94
- existing = await this.db.getKeyFromIndex(this.storeName, _IndexedDbArchivist.hashIndexName, hash);
95
- if (existing)
96
- await this.db.delete(this.storeName, existing);
97
- } while (!existing);
98
- return hash;
99
- }));
100
- return found.filter(exists);
93
+ return await this.useDb(async (db) => {
94
+ const found = await Promise.all(distinctHashes.map(async (hash) => {
95
+ const existing = await db.getKeyFromIndex(this.storeName, _IndexedDbArchivist.hashIndexName, hash);
96
+ if (existing) {
97
+ await db.delete(this.storeName, existing);
98
+ return hash;
99
+ }
100
+ }));
101
+ return found.filter(exists);
102
+ });
101
103
  }
102
104
  async getHandler(hashes) {
103
- const payloads = await Promise.all(hashes.map((hash) => this.db.getFromIndex(this.storeName, _IndexedDbArchivist.hashIndexName, hash)));
105
+ const payloads = await this.useDb((db) => Promise.all(hashes.map((hash) => db.getFromIndex(this.storeName, _IndexedDbArchivist.hashIndexName, hash))));
104
106
  return payloads.filter(exists);
105
107
  }
106
108
  async insertHandler(payloads) {
107
109
  const pairs = await PayloadHasher.hashPairs(payloads);
108
- const inserted = await Promise.all(pairs.map(async ([payload, _hash]) => {
109
- const tx = this.db.transaction(this.storeName, "readwrite");
110
- try {
111
- const store = tx.objectStore(this.storeName);
112
- const existing = await store.index(_IndexedDbArchivist.hashIndexName).get(_hash);
113
- if (!existing) {
114
- await store.put({
115
- ...payload,
116
- _hash
117
- });
118
- return payload;
110
+ const db = await this.getInitializedDb();
111
+ try {
112
+ const inserted = await Promise.all(pairs.map(async ([payload, _hash]) => {
113
+ const tx = db.transaction(this.storeName, "readwrite");
114
+ try {
115
+ const store = tx.objectStore(this.storeName);
116
+ const existing = await store.index(_IndexedDbArchivist.hashIndexName).get(_hash);
117
+ if (!existing) {
118
+ await store.put({
119
+ ...payload,
120
+ _hash
121
+ });
122
+ return payload;
123
+ }
124
+ } finally {
125
+ await tx.done;
119
126
  }
120
- } finally {
121
- await tx.done;
122
- }
123
- }));
124
- return inserted.filter(exists);
127
+ }));
128
+ return inserted.filter(exists);
129
+ } finally {
130
+ db.close();
131
+ }
125
132
  }
126
133
  async startHandler() {
127
134
  await super.startHandler();
135
+ await this.useDb(() => {
136
+ });
137
+ return true;
138
+ }
139
+ /**
140
+ * Returns that the desired DB/Store initialized to the correct version
141
+ * @returns The initialized DB
142
+ */
143
+ async getInitializedDb() {
128
144
  const { dbName, dbVersion, indexes, storeName } = this;
129
- this._db = await openDB(dbName, dbVersion, {
130
- async upgrade(database) {
131
- await Promise.resolve();
145
+ const db = await openDB(dbName, dbVersion, {
146
+ blocked(currentVersion, blockedVersion, event) {
147
+ console.warn(`IndexedDbArchivist: Blocked from upgrading from ${currentVersion} to ${blockedVersion}`, event);
148
+ },
149
+ blocking(currentVersion, blockedVersion, event) {
150
+ console.warn(`IndexedDbArchivist: Blocking upgrade from ${currentVersion} to ${blockedVersion}`, event);
151
+ },
152
+ terminated() {
153
+ console.log("IndexedDbArchivist: Terminated");
154
+ },
155
+ upgrade(database, oldVersion, newVersion, transaction) {
156
+ if (oldVersion !== newVersion) {
157
+ console.log(`IndexedDbArchivist: Upgrading from ${oldVersion} to ${newVersion}`);
158
+ const objectStores = transaction.objectStoreNames;
159
+ for (const name of objectStores) {
160
+ try {
161
+ database.deleteObjectStore(name);
162
+ } catch {
163
+ console.log(`IndexedDbArchivist: Failed to delete existing object store ${name}`);
164
+ }
165
+ }
166
+ }
132
167
  const store = database.createObjectStore(storeName, {
133
168
  // If it isn't explicitly set, create a value by auto incrementing.
134
169
  autoIncrement: true
135
170
  });
136
171
  store.name = storeName;
137
- const indexesToCreate = [
138
- _IndexedDbArchivist.hashIndex,
139
- _IndexedDbArchivist.schemaIndex,
140
- ...indexes
141
- ];
142
- for (const { key, multiEntry, unique } of indexesToCreate) {
172
+ for (const { key, multiEntry, unique } of indexes) {
143
173
  const indexKeys = Object.keys(key);
144
174
  const keys = indexKeys.length === 1 ? indexKeys[0] : indexKeys;
145
175
  const indexName = buildStandardIndexName({
@@ -153,7 +183,20 @@ var _IndexedDbArchivist = class _IndexedDbArchivist extends AbstractArchivist {
153
183
  }
154
184
  }
155
185
  });
156
- return true;
186
+ return db;
187
+ }
188
+ /**
189
+ * Executes a callback with the initialized DB and then closes the db
190
+ * @param callback The method to execute with the initialized DB
191
+ * @returns
192
+ */
193
+ async useDb(callback) {
194
+ const db = await this.getInitializedDb();
195
+ try {
196
+ return await callback(db);
197
+ } finally {
198
+ db.close();
199
+ }
157
200
  }
158
201
  };
159
202
  __name(_IndexedDbArchivist, "IndexedDbArchivist");
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/Archivist.ts","../../src/Schema.ts","../../src/Config.ts"],"sourcesContent":["import { assertEx } from '@xylabs/assert'\nimport { exists } from '@xylabs/exists'\nimport { AbstractArchivist } from '@xyo-network/archivist-abstract'\nimport {\n ArchivistAllQuerySchema,\n ArchivistClearQuerySchema,\n ArchivistDeleteQuerySchema,\n ArchivistInsertQuerySchema,\n ArchivistModuleEventData,\n buildStandardIndexName,\n IndexDescription,\n} from '@xyo-network/archivist-model'\nimport { PayloadHasher } from '@xyo-network/hash'\nimport { creatableModule } from '@xyo-network/module-model'\nimport { Payload } from '@xyo-network/payload-model'\nimport { IDBPDatabase, openDB } from 'idb'\n\nimport { IndexedDbArchivistConfigSchema } from './Config'\nimport { IndexedDbArchivistParams } from './Params'\n\nexport interface PayloadStore {\n [s: string]: Payload\n}\n\n@creatableModule()\nexport class IndexedDbArchivist<\n TParams extends IndexedDbArchivistParams = IndexedDbArchivistParams,\n TEventData extends ArchivistModuleEventData = ArchivistModuleEventData,\n> extends AbstractArchivist<TParams, TEventData> {\n static override configSchemas = [IndexedDbArchivistConfigSchema]\n static readonly defaultDbName = 'archivist'\n static readonly defaultDbVersion = 1\n static readonly defaultStoreName = 'payloads'\n private static readonly hashIndex: IndexDescription = { key: { _hash: 1 }, multiEntry: false, unique: true }\n private static readonly schemaIndex: IndexDescription = { key: { schema: 1 }, multiEntry: false, unique: false }\n // eslint-disable-next-line @typescript-eslint/member-ordering\n static readonly hashIndexName = buildStandardIndexName(IndexedDbArchivist.hashIndex)\n // eslint-disable-next-line @typescript-eslint/member-ordering\n static readonly schemaIndexName = buildStandardIndexName(IndexedDbArchivist.schemaIndex)\n\n private _db: IDBPDatabase<PayloadStore> | undefined\n\n /**\n * The database name. If not supplied via config, it defaults\n * to the module name (not guaranteed to be unique) and if module\n * name is not supplied, it defaults to `archivist`. This behavior\n * biases towards a single, isolated DB per archivist which seems to\n * make the most sense for 99% of use cases.\n */\n get dbName() {\n return this.config?.dbName ?? this.config?.name ?? IndexedDbArchivist.defaultDbName\n }\n\n /**\n * The database version. If not supplied via config, it defaults to 1.\n */\n get dbVersion() {\n return this.config?.dbVersion ?? IndexedDbArchivist.defaultDbVersion\n }\n\n override get queries() {\n return [ArchivistAllQuerySchema, ArchivistClearQuerySchema, ArchivistDeleteQuerySchema, ArchivistInsertQuerySchema, ...super.queries]\n }\n\n /**\n * The name of the object store. If not supplied via config, it defaults\n * to `payloads`.\n */\n get storeName() {\n return this.config?.storeName ?? IndexedDbArchivist.defaultStoreName\n }\n\n private get db(): IDBPDatabase<PayloadStore> {\n return assertEx(this._db, 'DB not initialized')\n }\n\n private get indexes() {\n return this.config?.storage?.indexes ?? []\n }\n\n protected override async allHandler(): Promise<Payload[]> {\n // Get all payloads from the store\n const payloads = await this.db.getAll(this.storeName)\n // Remove any metadata before returning to the client\n return payloads.map((payload) => PayloadHasher.jsonPayload(payload))\n }\n\n protected override async clearHandler(): Promise<void> {\n await this.db.clear(this.storeName)\n }\n\n protected override async deleteHandler(hashes: string[]): Promise<string[]> {\n const distinctHashes = [...new Set(hashes)]\n const found = await Promise.all(\n distinctHashes.map(async (hash) => {\n let existing: IDBValidKey | undefined\n do {\n existing = await this.db.getKeyFromIndex(this.storeName, IndexedDbArchivist.hashIndexName, hash)\n if (existing) await this.db.delete(this.storeName, existing)\n } while (!existing)\n return hash\n }),\n )\n // Return hashes removed\n return found.filter(exists)\n }\n\n protected override async getHandler(hashes: string[]): Promise<Payload[]> {\n const payloads = await Promise.all(hashes.map((hash) => this.db.getFromIndex(this.storeName, IndexedDbArchivist.hashIndexName, hash)))\n return payloads.filter(exists)\n }\n\n protected override async insertHandler(payloads: Payload[]): Promise<Payload[]> {\n const pairs = await PayloadHasher.hashPairs(payloads)\n // Only return the payloads that were successfully inserted\n const inserted = await Promise.all(\n pairs.map(async ([payload, _hash]) => {\n const tx = this.db.transaction(this.storeName, 'readwrite')\n try {\n const store = tx.objectStore(this.storeName)\n const existing = await store.index(IndexedDbArchivist.hashIndexName).get(_hash)\n if (!existing) {\n await store.put({ ...payload, _hash })\n return payload\n }\n } finally {\n await tx.done\n }\n }),\n )\n return inserted.filter(exists)\n }\n\n protected override async startHandler() {\n await super.startHandler()\n // NOTE: We could defer this creation to first access but we\n // want to fail fast here in case something is wrong\n const { dbName, dbVersion, indexes, storeName } = this\n this._db = await openDB<PayloadStore>(dbName, dbVersion, {\n async upgrade(database) {\n await Promise.resolve() // Async to match spec\n // Create the store\n const store = database.createObjectStore(storeName, {\n // If it isn't explicitly set, create a value by auto incrementing.\n autoIncrement: true,\n })\n // Name the store\n store.name = storeName\n // Create an index on the hash\n const indexesToCreate = [IndexedDbArchivist.hashIndex, IndexedDbArchivist.schemaIndex, ...indexes]\n for (const { key, multiEntry, unique } of indexesToCreate) {\n const indexKeys = Object.keys(key)\n const keys = indexKeys.length === 1 ? indexKeys[0] : indexKeys\n const indexName = buildStandardIndexName({ key, unique })\n store.createIndex(indexName, keys, { multiEntry, unique })\n }\n },\n })\n return true\n }\n}\n","export type IndexedDbArchivistSchema = 'network.xyo.archivist.indexeddb'\nexport const IndexedDbArchivistSchema: IndexedDbArchivistSchema = 'network.xyo.archivist.indexeddb'\n","import { ArchivistConfig, IndexDescription } from '@xyo-network/archivist-model'\n\nimport { IndexedDbArchivistSchema } from './Schema'\n\nexport type IndexedDbArchivistConfigSchema = `${IndexedDbArchivistSchema}.config`\nexport const IndexedDbArchivistConfigSchema: IndexedDbArchivistConfigSchema = `${IndexedDbArchivistSchema}.config`\n\nexport type IndexedDbArchivistConfig = ArchivistConfig<{\n /**\n * The database name\n */\n dbName?: string\n /**\n * The version of the DB, defaults to 1\n */\n dbVersion?: number\n schema: IndexedDbArchivistConfigSchema\n /**\n * The storage configuration\n * // TODO: Hoist to main archivist config\n */\n storage?: {\n /**\n * The indexes to create on the object store\n */\n indexes?: IndexDescription[]\n }\n /**\n * The name of the object store\n */\n storeName?: string\n}>\n"],"mappings":"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","names":["assertEx","exists","AbstractArchivist","ArchivistAllQuerySchema","ArchivistClearQuerySchema","ArchivistDeleteQuerySchema","ArchivistInsertQuerySchema","buildStandardIndexName","PayloadHasher","creatableModule","openDB","IndexedDbArchivistSchema","IndexedDbArchivistConfigSchema","IndexedDbArchivistSchema","IndexedDbArchivist","AbstractArchivist","_db","dbName","config","name","defaultDbName","dbVersion","defaultDbVersion","queries","ArchivistAllQuerySchema","ArchivistClearQuerySchema","ArchivistDeleteQuerySchema","ArchivistInsertQuerySchema","storeName","defaultStoreName","db","assertEx","indexes","storage","allHandler","payloads","getAll","map","payload","PayloadHasher","jsonPayload","clearHandler","clear","deleteHandler","hashes","distinctHashes","Set","found","Promise","all","hash","existing","getKeyFromIndex","hashIndexName","delete","filter","exists","getHandler","getFromIndex","insertHandler","pairs","hashPairs","inserted","_hash","tx","transaction","store","objectStore","index","get","put","done","startHandler","openDB","upgrade","database","resolve","createObjectStore","autoIncrement","indexesToCreate","hashIndex","schemaIndex","key","multiEntry","unique","indexKeys","Object","keys","length","indexName","buildStandardIndexName","createIndex","configSchemas","IndexedDbArchivistConfigSchema","schema","schemaIndexName","creatableModule"]}
1
+ {"version":3,"sources":["../../src/Archivist.ts","../../src/Schema.ts","../../src/Config.ts"],"sourcesContent":["import { exists } from '@xylabs/exists'\nimport { AbstractArchivist } from '@xyo-network/archivist-abstract'\nimport {\n ArchivistAllQuerySchema,\n ArchivistClearQuerySchema,\n ArchivistDeleteQuerySchema,\n ArchivistInsertQuerySchema,\n ArchivistModuleEventData,\n buildStandardIndexName,\n IndexDescription,\n} from '@xyo-network/archivist-model'\nimport { PayloadHasher } from '@xyo-network/hash'\nimport { creatableModule } from '@xyo-network/module-model'\nimport { Payload } from '@xyo-network/payload-model'\nimport { IDBPDatabase, openDB } from 'idb'\n\nimport { IndexedDbArchivistConfigSchema } from './Config'\nimport { IndexedDbArchivistParams } from './Params'\n\nexport interface PayloadStore {\n [s: string]: Payload\n}\n\n@creatableModule()\nexport class IndexedDbArchivist<\n TParams extends IndexedDbArchivistParams = IndexedDbArchivistParams,\n TEventData extends ArchivistModuleEventData = ArchivistModuleEventData,\n> extends AbstractArchivist<TParams, TEventData> {\n static override configSchemas = [IndexedDbArchivistConfigSchema]\n static readonly defaultDbName = 'archivist'\n static readonly defaultDbVersion = 1\n static readonly defaultStoreName = 'payloads'\n private static readonly hashIndex: IndexDescription = { key: { _hash: 1 }, multiEntry: false, unique: true }\n private static readonly schemaIndex: IndexDescription = { key: { schema: 1 }, multiEntry: false, unique: false }\n // eslint-disable-next-line @typescript-eslint/member-ordering\n static readonly hashIndexName = buildStandardIndexName(IndexedDbArchivist.hashIndex)\n // eslint-disable-next-line @typescript-eslint/member-ordering\n static readonly schemaIndexName = buildStandardIndexName(IndexedDbArchivist.schemaIndex)\n\n /**\n * The database name. If not supplied via config, it defaults\n * to the module name (not guaranteed to be unique) and if module\n * name is not supplied, it defaults to `archivist`. This behavior\n * biases towards a single, isolated DB per archivist which seems to\n * make the most sense for 99% of use cases.\n */\n get dbName() {\n return this.config?.dbName ?? this.config?.name ?? IndexedDbArchivist.defaultDbName\n }\n\n /**\n * The database version. If not supplied via config, it defaults to 1.\n */\n get dbVersion() {\n return this.config?.dbVersion ?? IndexedDbArchivist.defaultDbVersion\n }\n\n override get queries() {\n return [ArchivistAllQuerySchema, ArchivistClearQuerySchema, ArchivistDeleteQuerySchema, ArchivistInsertQuerySchema, ...super.queries]\n }\n\n /**\n * The name of the object store. If not supplied via config, it defaults\n * to `payloads`.\n */\n get storeName() {\n return this.config?.storeName ?? IndexedDbArchivist.defaultStoreName\n }\n\n /**\n * The indexes to create on the store\n */\n private get indexes() {\n return [IndexedDbArchivist.hashIndex, IndexedDbArchivist.schemaIndex, ...(this.config?.storage?.indexes ?? [])]\n }\n\n protected override async allHandler(): Promise<Payload[]> {\n // Get all payloads from the store\n const payloads = await this.useDb((db) => db.getAll(this.storeName))\n // Remove any metadata before returning to the client\n return payloads.map((payload) => PayloadHasher.jsonPayload(payload))\n }\n\n protected override async clearHandler(): Promise<void> {\n await this.useDb((db) => db.clear(this.storeName))\n }\n\n protected override async deleteHandler(hashes: string[]): Promise<string[]> {\n // Remove any duplicates\n const distinctHashes = [...new Set(hashes)]\n return await this.useDb(async (db) => {\n // Only return hashes that were successfully deleted\n const found = await Promise.all(\n distinctHashes.map(async (hash) => {\n // Check if the hash exists\n const existing = await db.getKeyFromIndex(this.storeName, IndexedDbArchivist.hashIndexName, hash)\n // If it does exist\n if (existing) {\n // Delete it\n await db.delete(this.storeName, existing)\n // Return the hash so it gets added to the list of deleted hashes\n return hash\n }\n }),\n )\n return found.filter(exists)\n })\n }\n\n protected override async getHandler(hashes: string[]): Promise<Payload[]> {\n const payloads = await this.useDb((db) =>\n Promise.all(hashes.map((hash) => db.getFromIndex(this.storeName, IndexedDbArchivist.hashIndexName, hash))),\n )\n return payloads.filter(exists)\n }\n\n protected override async insertHandler(payloads: Payload[]): Promise<Payload[]> {\n const pairs = await PayloadHasher.hashPairs(payloads)\n const db = await this.getInitializedDb()\n try {\n // Only return the payloads that were successfully inserted\n const inserted = await Promise.all(\n pairs.map(async ([payload, _hash]) => {\n // Perform each insert via a transaction to ensure it is atomic\n // with respect to checking for the pre-existence of the hash.\n // This is done to preserve iteration via insertion order.\n const tx = db.transaction(this.storeName, 'readwrite')\n try {\n // Get the object store\n const store = tx.objectStore(this.storeName)\n // Check if the hash already exists\n const existing = await store.index(IndexedDbArchivist.hashIndexName).get(_hash)\n // If it does not already exist\n if (!existing) {\n // Insert the payload\n await store.put({ ...payload, _hash })\n // Return it so it gets added to the list of inserted payloads\n return payload\n }\n } finally {\n // Close the transaction\n await tx.done\n }\n }),\n )\n return inserted.filter(exists)\n } finally {\n db.close()\n }\n }\n\n protected override async startHandler() {\n await super.startHandler()\n // NOTE: We could defer this creation to first access but we\n // want to fail fast here in case something is wrong\n await this.useDb(() => {})\n return true\n }\n\n /**\n * Returns that the desired DB/Store initialized to the correct version\n * @returns The initialized DB\n */\n private async getInitializedDb(): Promise<IDBPDatabase<PayloadStore>> {\n const { dbName, dbVersion, indexes, storeName } = this\n const db = await openDB<PayloadStore>(dbName, dbVersion, {\n blocked(currentVersion, blockedVersion, event) {\n console.warn(`IndexedDbArchivist: Blocked from upgrading from ${currentVersion} to ${blockedVersion}`, event)\n },\n blocking(currentVersion, blockedVersion, event) {\n console.warn(`IndexedDbArchivist: Blocking upgrade from ${currentVersion} to ${blockedVersion}`, event)\n },\n terminated() {\n console.log('IndexedDbArchivist: Terminated')\n },\n upgrade(database, oldVersion, newVersion, transaction) {\n // NOTE: This is called whenever the DB is created/updated. We could simply ensure the desired end\n // state but, out of an abundance of caution, we will just delete (so we know where we are starting\n // from a known good point) and recreate the desired state. This prioritizes resilience over data\n // retention but we can revisit that tradeoff when it becomes limiting. Because distributed browser\n // state is extremely hard to debug, this seems like fair tradeoff for now.\n if (oldVersion !== newVersion) {\n console.log(`IndexedDbArchivist: Upgrading from ${oldVersion} to ${newVersion}`)\n // Delete any existing databases that are not the current version\n const objectStores = transaction.objectStoreNames\n for (const name of objectStores) {\n try {\n database.deleteObjectStore(name)\n } catch {\n console.log(`IndexedDbArchivist: Failed to delete existing object store ${name}`)\n }\n }\n }\n // Create the store\n const store = database.createObjectStore(storeName, {\n // If it isn't explicitly set, create a value by auto incrementing.\n autoIncrement: true,\n })\n // Name the store\n store.name = storeName\n // Create an index on the hash\n for (const { key, multiEntry, unique } of indexes) {\n const indexKeys = Object.keys(key)\n const keys = indexKeys.length === 1 ? indexKeys[0] : indexKeys\n const indexName = buildStandardIndexName({ key, unique })\n store.createIndex(indexName, keys, { multiEntry, unique })\n }\n },\n })\n return db\n }\n\n /**\n * Executes a callback with the initialized DB and then closes the db\n * @param callback The method to execute with the initialized DB\n * @returns\n */\n private async useDb<T>(callback: (db: IDBPDatabase<PayloadStore>) => Promise<T> | T): Promise<T> {\n // Get the initialized DB\n const db = await this.getInitializedDb()\n try {\n // Perform the callback\n return await callback(db)\n } finally {\n // Close the DB\n db.close()\n }\n }\n}\n","export type IndexedDbArchivistSchema = 'network.xyo.archivist.indexeddb'\nexport const IndexedDbArchivistSchema: IndexedDbArchivistSchema = 'network.xyo.archivist.indexeddb'\n","import { ArchivistConfig, IndexDescription } from '@xyo-network/archivist-model'\n\nimport { IndexedDbArchivistSchema } from './Schema'\n\nexport type IndexedDbArchivistConfigSchema = `${IndexedDbArchivistSchema}.config`\nexport const IndexedDbArchivistConfigSchema: IndexedDbArchivistConfigSchema = `${IndexedDbArchivistSchema}.config`\n\nexport type IndexedDbArchivistConfig = ArchivistConfig<{\n /**\n * The database name\n */\n dbName?: string\n /**\n * The version of the DB, defaults to 1\n */\n dbVersion?: number\n schema: IndexedDbArchivistConfigSchema\n /**\n * The storage configuration\n * // TODO: Hoist to main archivist config\n */\n storage?: {\n /**\n * The indexes to create on the object store\n */\n indexes?: IndexDescription[]\n }\n /**\n * The name of the object store\n */\n storeName?: string\n}>\n"],"mappings":"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","names":["exists","AbstractArchivist","ArchivistAllQuerySchema","ArchivistClearQuerySchema","ArchivistDeleteQuerySchema","ArchivistInsertQuerySchema","buildStandardIndexName","PayloadHasher","creatableModule","openDB","IndexedDbArchivistSchema","IndexedDbArchivistConfigSchema","IndexedDbArchivistSchema","IndexedDbArchivist","AbstractArchivist","dbName","config","name","defaultDbName","dbVersion","defaultDbVersion","queries","ArchivistAllQuerySchema","ArchivistClearQuerySchema","ArchivistDeleteQuerySchema","ArchivistInsertQuerySchema","storeName","defaultStoreName","indexes","hashIndex","schemaIndex","storage","allHandler","payloads","useDb","db","getAll","map","payload","PayloadHasher","jsonPayload","clearHandler","clear","deleteHandler","hashes","distinctHashes","Set","found","Promise","all","hash","existing","getKeyFromIndex","hashIndexName","delete","filter","exists","getHandler","getFromIndex","insertHandler","pairs","hashPairs","getInitializedDb","inserted","_hash","tx","transaction","store","objectStore","index","get","put","done","close","startHandler","openDB","blocked","currentVersion","blockedVersion","event","console","warn","blocking","terminated","log","upgrade","database","oldVersion","newVersion","objectStores","objectStoreNames","deleteObjectStore","createObjectStore","autoIncrement","key","multiEntry","unique","indexKeys","Object","keys","length","indexName","buildStandardIndexName","createIndex","callback","configSchemas","IndexedDbArchivistConfigSchema","schema","schemaIndexName","creatableModule"]}
package/package.json CHANGED
@@ -10,21 +10,21 @@
10
10
  "url": "https://github.com/XYOracleNetwork/sdk-xyo-client-js/issues"
11
11
  },
12
12
  "dependencies": {
13
- "@xylabs/assert": "^2.13.23",
14
13
  "@xylabs/exists": "^2.13.23",
15
- "@xyo-network/archivist-abstract": "~2.87.1",
16
- "@xyo-network/archivist-model": "~2.87.1",
17
- "@xyo-network/hash": "~2.87.1",
18
- "@xyo-network/module-model": "~2.87.1",
19
- "@xyo-network/payload-model": "~2.87.1",
14
+ "@xyo-network/archivist-abstract": "~2.88.0",
15
+ "@xyo-network/archivist-model": "~2.88.0",
16
+ "@xyo-network/hash": "~2.88.0",
17
+ "@xyo-network/module-model": "~2.88.0",
18
+ "@xyo-network/payload-model": "~2.88.0",
20
19
  "idb": "^8.0.0"
21
20
  },
22
21
  "devDependencies": {
22
+ "@xylabs/assert": "^2.13.23",
23
23
  "@xylabs/ts-scripts-yarn3": "^3.2.41",
24
24
  "@xylabs/tsconfig": "^3.2.41",
25
- "@xyo-network/account": "~2.87.1",
26
- "@xyo-network/id-payload-plugin": "~2.87.1",
27
- "@xyo-network/payload-wrapper": "~2.87.1",
25
+ "@xyo-network/account": "~2.88.0",
26
+ "@xyo-network/id-payload-plugin": "~2.88.0",
27
+ "@xyo-network/payload-wrapper": "~2.88.0",
28
28
  "fake-indexeddb": "^5.0.2",
29
29
  "typescript": "^5.3.3"
30
30
  },
@@ -67,6 +67,6 @@
67
67
  "url": "https://github.com/XYOracleNetwork/sdk-xyo-client-js.git"
68
68
  },
69
69
  "sideEffects": false,
70
- "version": "2.87.1",
70
+ "version": "2.88.0",
71
71
  "type": "module"
72
72
  }
package/src/Archivist.ts CHANGED
@@ -1,4 +1,3 @@
1
- import { assertEx } from '@xylabs/assert'
2
1
  import { exists } from '@xylabs/exists'
3
2
  import { AbstractArchivist } from '@xyo-network/archivist-abstract'
4
3
  import {
@@ -38,8 +37,6 @@ export class IndexedDbArchivist<
38
37
  // eslint-disable-next-line @typescript-eslint/member-ordering
39
38
  static readonly schemaIndexName = buildStandardIndexName(IndexedDbArchivist.schemaIndex)
40
39
 
41
- private _db: IDBPDatabase<PayloadStore> | undefined
42
-
43
40
  /**
44
41
  * The database name. If not supplied via config, it defaults
45
42
  * to the module name (not guaranteed to be unique) and if module
@@ -70,75 +67,130 @@ export class IndexedDbArchivist<
70
67
  return this.config?.storeName ?? IndexedDbArchivist.defaultStoreName
71
68
  }
72
69
 
73
- private get db(): IDBPDatabase<PayloadStore> {
74
- return assertEx(this._db, 'DB not initialized')
75
- }
76
-
70
+ /**
71
+ * The indexes to create on the store
72
+ */
77
73
  private get indexes() {
78
- return this.config?.storage?.indexes ?? []
74
+ return [IndexedDbArchivist.hashIndex, IndexedDbArchivist.schemaIndex, ...(this.config?.storage?.indexes ?? [])]
79
75
  }
80
76
 
81
77
  protected override async allHandler(): Promise<Payload[]> {
82
78
  // Get all payloads from the store
83
- const payloads = await this.db.getAll(this.storeName)
79
+ const payloads = await this.useDb((db) => db.getAll(this.storeName))
84
80
  // Remove any metadata before returning to the client
85
81
  return payloads.map((payload) => PayloadHasher.jsonPayload(payload))
86
82
  }
87
83
 
88
84
  protected override async clearHandler(): Promise<void> {
89
- await this.db.clear(this.storeName)
85
+ await this.useDb((db) => db.clear(this.storeName))
90
86
  }
91
87
 
92
88
  protected override async deleteHandler(hashes: string[]): Promise<string[]> {
89
+ // Remove any duplicates
93
90
  const distinctHashes = [...new Set(hashes)]
94
- const found = await Promise.all(
95
- distinctHashes.map(async (hash) => {
96
- let existing: IDBValidKey | undefined
97
- do {
98
- existing = await this.db.getKeyFromIndex(this.storeName, IndexedDbArchivist.hashIndexName, hash)
99
- if (existing) await this.db.delete(this.storeName, existing)
100
- } while (!existing)
101
- return hash
102
- }),
103
- )
104
- // Return hashes removed
105
- return found.filter(exists)
91
+ return await this.useDb(async (db) => {
92
+ // Only return hashes that were successfully deleted
93
+ const found = await Promise.all(
94
+ distinctHashes.map(async (hash) => {
95
+ // Check if the hash exists
96
+ const existing = await db.getKeyFromIndex(this.storeName, IndexedDbArchivist.hashIndexName, hash)
97
+ // If it does exist
98
+ if (existing) {
99
+ // Delete it
100
+ await db.delete(this.storeName, existing)
101
+ // Return the hash so it gets added to the list of deleted hashes
102
+ return hash
103
+ }
104
+ }),
105
+ )
106
+ return found.filter(exists)
107
+ })
106
108
  }
107
109
 
108
110
  protected override async getHandler(hashes: string[]): Promise<Payload[]> {
109
- const payloads = await Promise.all(hashes.map((hash) => this.db.getFromIndex(this.storeName, IndexedDbArchivist.hashIndexName, hash)))
111
+ const payloads = await this.useDb((db) =>
112
+ Promise.all(hashes.map((hash) => db.getFromIndex(this.storeName, IndexedDbArchivist.hashIndexName, hash))),
113
+ )
110
114
  return payloads.filter(exists)
111
115
  }
112
116
 
113
117
  protected override async insertHandler(payloads: Payload[]): Promise<Payload[]> {
114
118
  const pairs = await PayloadHasher.hashPairs(payloads)
115
- // Only return the payloads that were successfully inserted
116
- const inserted = await Promise.all(
117
- pairs.map(async ([payload, _hash]) => {
118
- const tx = this.db.transaction(this.storeName, 'readwrite')
119
- try {
120
- const store = tx.objectStore(this.storeName)
121
- const existing = await store.index(IndexedDbArchivist.hashIndexName).get(_hash)
122
- if (!existing) {
123
- await store.put({ ...payload, _hash })
124
- return payload
119
+ const db = await this.getInitializedDb()
120
+ try {
121
+ // Only return the payloads that were successfully inserted
122
+ const inserted = await Promise.all(
123
+ pairs.map(async ([payload, _hash]) => {
124
+ // Perform each insert via a transaction to ensure it is atomic
125
+ // with respect to checking for the pre-existence of the hash.
126
+ // This is done to preserve iteration via insertion order.
127
+ const tx = db.transaction(this.storeName, 'readwrite')
128
+ try {
129
+ // Get the object store
130
+ const store = tx.objectStore(this.storeName)
131
+ // Check if the hash already exists
132
+ const existing = await store.index(IndexedDbArchivist.hashIndexName).get(_hash)
133
+ // If it does not already exist
134
+ if (!existing) {
135
+ // Insert the payload
136
+ await store.put({ ...payload, _hash })
137
+ // Return it so it gets added to the list of inserted payloads
138
+ return payload
139
+ }
140
+ } finally {
141
+ // Close the transaction
142
+ await tx.done
125
143
  }
126
- } finally {
127
- await tx.done
128
- }
129
- }),
130
- )
131
- return inserted.filter(exists)
144
+ }),
145
+ )
146
+ return inserted.filter(exists)
147
+ } finally {
148
+ db.close()
149
+ }
132
150
  }
133
151
 
134
152
  protected override async startHandler() {
135
153
  await super.startHandler()
136
154
  // NOTE: We could defer this creation to first access but we
137
155
  // want to fail fast here in case something is wrong
156
+ await this.useDb(() => {})
157
+ return true
158
+ }
159
+
160
+ /**
161
+ * Returns that the desired DB/Store initialized to the correct version
162
+ * @returns The initialized DB
163
+ */
164
+ private async getInitializedDb(): Promise<IDBPDatabase<PayloadStore>> {
138
165
  const { dbName, dbVersion, indexes, storeName } = this
139
- this._db = await openDB<PayloadStore>(dbName, dbVersion, {
140
- async upgrade(database) {
141
- await Promise.resolve() // Async to match spec
166
+ const db = await openDB<PayloadStore>(dbName, dbVersion, {
167
+ blocked(currentVersion, blockedVersion, event) {
168
+ console.warn(`IndexedDbArchivist: Blocked from upgrading from ${currentVersion} to ${blockedVersion}`, event)
169
+ },
170
+ blocking(currentVersion, blockedVersion, event) {
171
+ console.warn(`IndexedDbArchivist: Blocking upgrade from ${currentVersion} to ${blockedVersion}`, event)
172
+ },
173
+ terminated() {
174
+ console.log('IndexedDbArchivist: Terminated')
175
+ },
176
+ upgrade(database, oldVersion, newVersion, transaction) {
177
+ // NOTE: This is called whenever the DB is created/updated. We could simply ensure the desired end
178
+ // state but, out of an abundance of caution, we will just delete (so we know where we are starting
179
+ // from a known good point) and recreate the desired state. This prioritizes resilience over data
180
+ // retention but we can revisit that tradeoff when it becomes limiting. Because distributed browser
181
+ // state is extremely hard to debug, this seems like fair tradeoff for now.
182
+ if (oldVersion !== newVersion) {
183
+ console.log(`IndexedDbArchivist: Upgrading from ${oldVersion} to ${newVersion}`)
184
+ // Delete any existing databases that are not the current version
185
+ const objectStores = transaction.objectStoreNames
186
+ for (const name of objectStores) {
187
+ try {
188
+ database.deleteObjectStore(name)
189
+ } catch {
190
+ console.log(`IndexedDbArchivist: Failed to delete existing object store ${name}`)
191
+ }
192
+ }
193
+ }
142
194
  // Create the store
143
195
  const store = database.createObjectStore(storeName, {
144
196
  // If it isn't explicitly set, create a value by auto incrementing.
@@ -147,8 +199,7 @@ export class IndexedDbArchivist<
147
199
  // Name the store
148
200
  store.name = storeName
149
201
  // Create an index on the hash
150
- const indexesToCreate = [IndexedDbArchivist.hashIndex, IndexedDbArchivist.schemaIndex, ...indexes]
151
- for (const { key, multiEntry, unique } of indexesToCreate) {
202
+ for (const { key, multiEntry, unique } of indexes) {
152
203
  const indexKeys = Object.keys(key)
153
204
  const keys = indexKeys.length === 1 ? indexKeys[0] : indexKeys
154
205
  const indexName = buildStandardIndexName({ key, unique })
@@ -156,6 +207,23 @@ export class IndexedDbArchivist<
156
207
  }
157
208
  },
158
209
  })
159
- return true
210
+ return db
211
+ }
212
+
213
+ /**
214
+ * Executes a callback with the initialized DB and then closes the db
215
+ * @param callback The method to execute with the initialized DB
216
+ * @returns
217
+ */
218
+ private async useDb<T>(callback: (db: IDBPDatabase<PayloadStore>) => Promise<T> | T): Promise<T> {
219
+ // Get the initialized DB
220
+ const db = await this.getInitializedDb()
221
+ try {
222
+ // Perform the callback
223
+ return await callback(db)
224
+ } finally {
225
+ // Close the DB
226
+ db.close()
227
+ }
160
228
  }
161
229
  }