@nxgt/mongo-meilisearch 0.2.0 → 0.3.1

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.
package/dist/index.js.map CHANGED
@@ -1,18 +1,20 @@
1
1
  {
2
2
  "version": 3,
3
- "sources": ["../src/context.ts", "../src/batch.ts", "../src/errors.ts", "../src/documents.ts", "../src/reindex.ts", "../src/running.ts", "../src/state.ts", "../src/follow.ts", "../src/create-search-sync.ts"],
3
+ "sources": ["../src/context.ts", "../src/reindex.ts", "../src/batch.ts", "../src/errors.ts", "../src/documents.ts", "../src/lease.ts", "../src/running.ts", "../src/state.ts", "../src/follow.ts", "../src/start.ts", "../src/create-search-sync.ts"],
4
4
  "sourcesContent": [
5
- "import type { AnyIndexDefinition, TypedIndex } from '@nxgt/meilisearch';\nimport type { AnyCollectionDefinition, TypedCollection } from '@nxgt/mongo';\nimport type { Collection } from 'mongodb';\nimport type { SearchSyncOptions, SearchSyncState } from './types';\n\n/** Loosely typed: this layer works on any documents; the public types are what callers see. */\nexport type Doc = Record<string, unknown>;\n\n/**\n * What a sync works from, resolved once. Data, and the caller's own two\n * functions as they were given.\n */\nexport interface SyncContext {\n\treadonly name: string;\n\treadonly collection: TypedCollection<AnyCollectionDefinition>;\n\treadonly index: TypedIndex<AnyIndexDefinition>;\n\treadonly primaryKey: string;\n\treadonly state: Collection<SearchSyncState>;\n\treadonly batchSize: number;\n\treadonly flushIntervalMs: number;\n\treadonly positionIntervalMs: number;\n\treadonly pageSize: number;\n\treadonly onHistoryLost: 'reindex' | 'fail';\n\treadonly transform: (document: Doc) => unknown;\n\treadonly toIndexId: (id: unknown) => unknown;\n\t/**\n\t * Whether this sync is following changes in this process. A reindex\n\t * beside its own follower would remove what the follower has just\n\t * indexed, and the follower would never send it again.\n\t */\n\treadonly follower: { running: boolean };\n}\n\nconst DEFAULT_STATE_COLLECTION = 'nxgt_search_sync';\n\nfunction positive(option: string, value: number | undefined, fallback: number) {\n\tconst resolved = value ?? fallback;\n\tif (!Number.isInteger(resolved) || resolved < 1) {\n\t\tthrow new TypeError(\n\t\t\t`createSearchSync: ${option} must be a whole number above 0, not ${String(value)}`,\n\t\t);\n\t}\n\treturn resolved;\n}\n\nexport function createContext(\n\toptions: SearchSyncOptions<AnyCollectionDefinition, AnyIndexDefinition>,\n): SyncContext {\n\tconst { collection, index } = options;\n\tif (typeof options.transform !== 'function') {\n\t\tthrow new TypeError('createSearchSync: transform must be a function');\n\t}\n\tconst name = options.name ?? `${collection.collectionName}:${index.uid}`;\n\tif (name === '') {\n\t\tthrow new TypeError('createSearchSync: name must not be empty');\n\t}\n\tconst flushIntervalMs = options.flushIntervalMs ?? 1000;\n\tif (!Number.isInteger(flushIntervalMs) || flushIntervalMs < 0) {\n\t\tthrow new TypeError(\n\t\t\t`createSearchSync: flushIntervalMs must be a whole number of milliseconds, not ${String(options.flushIntervalMs)}`,\n\t\t);\n\t}\n\treturn {\n\t\tname,\n\t\tcollection,\n\t\tindex,\n\t\tprimaryKey: index.definition.primaryKey as string,\n\t\tstate: collection.db.collection<SearchSyncState>(\n\t\t\toptions.stateCollection ?? DEFAULT_STATE_COLLECTION,\n\t\t),\n\t\tbatchSize: positive('batchSize', options.batchSize, 500),\n\t\tpositionIntervalMs: positive(\n\t\t\t'positionIntervalMs',\n\t\t\toptions.positionIntervalMs,\n\t\t\t60_000,\n\t\t),\n\t\tflushIntervalMs,\n\t\tpageSize: positive('pageSize', options.pageSize, 100),\n\t\tonHistoryLost: options.onHistoryLost ?? 'reindex',\n\t\ttransform: options.transform as (document: Doc) => unknown,\n\t\ttoIndexId: (options.toIndexId ?? String) as (id: unknown) => unknown,\n\t\tfollower: { running: false },\n\t};\n}\n",
5
+ "import type { AnyIndexDefinition, TypedIndex } from '@nxgt/meilisearch';\nimport type { AnyCollectionDefinition, TypedCollection } from '@nxgt/mongo';\nimport type { Collection } from 'mongodb';\nimport type { LeaseDocument } from './lease';\nimport type { SearchSyncOptions, SearchSyncState } from './types';\n\n/** Loosely typed: this layer works on any documents; the public types are what callers see. */\nexport type Doc = Record<string, unknown>;\n\n/**\n * What a sync works from, resolved once. Data, and the caller's own two\n * functions as they were given.\n */\nexport interface SyncContext {\n\treadonly name: string;\n\treadonly collection: TypedCollection<AnyCollectionDefinition>;\n\treadonly index: TypedIndex<AnyIndexDefinition>;\n\treadonly primaryKey: string;\n\treadonly state: Collection<SearchSyncState>;\n\t/** The same collection, for the lease documents it also holds. */\n\treadonly leases: Collection<LeaseDocument>;\n\t/** How long a lease lasts unrenewed, in ms. */\n\treadonly leaseMs: number;\n\treadonly batchSize: number;\n\treadonly flushIntervalMs: number;\n\treadonly positionIntervalMs: number;\n\treadonly pageSize: number;\n\treadonly onHistoryLost: 'reindex' | 'fail';\n\treadonly transform: (document: Doc) => unknown;\n\treadonly toIndexId: (id: unknown) => unknown;\n\t/**\n\t * Whether this sync is following changes in this process. A reindex\n\t * beside its own follower would remove what the follower has just\n\t * indexed, and the follower would never send it again.\n\t */\n\treadonly follower: { running: boolean };\n}\n\nconst DEFAULT_STATE_COLLECTION = 'nxgt_search_sync';\n\nfunction positive(option: string, value: number | undefined, fallback: number) {\n\tconst resolved = value ?? fallback;\n\tif (!Number.isInteger(resolved) || resolved < 1) {\n\t\tthrow new TypeError(\n\t\t\t`createSearchSync: ${option} must be a whole number above 0, not ${String(value)}`,\n\t\t);\n\t}\n\treturn resolved;\n}\n\nexport function createContext(\n\toptions: SearchSyncOptions<AnyCollectionDefinition, AnyIndexDefinition>,\n): SyncContext {\n\tconst { collection, index } = options;\n\tif (typeof options.transform !== 'function') {\n\t\tthrow new TypeError('createSearchSync: transform must be a function');\n\t}\n\tconst name = options.name ?? `${collection.collectionName}:${index.uid}`;\n\tif (name === '') {\n\t\tthrow new TypeError('createSearchSync: name must not be empty');\n\t}\n\tconst flushIntervalMs = options.flushIntervalMs ?? 1000;\n\tif (!Number.isInteger(flushIntervalMs) || flushIntervalMs < 0) {\n\t\tthrow new TypeError(\n\t\t\t`createSearchSync: flushIntervalMs must be a whole number of milliseconds, not ${String(options.flushIntervalMs)}`,\n\t\t);\n\t}\n\tconst stateCollection = options.stateCollection ?? DEFAULT_STATE_COLLECTION;\n\treturn {\n\t\tname,\n\t\tcollection,\n\t\tindex,\n\t\tprimaryKey: index.definition.primaryKey as string,\n\t\tstate: collection.db.collection<SearchSyncState>(stateCollection),\n\t\tleases: collection.db.collection<LeaseDocument>(stateCollection),\n\t\tleaseMs: positive('leaseMs', options.leaseMs, 30_000),\n\t\tbatchSize: positive('batchSize', options.batchSize, 500),\n\t\tpositionIntervalMs: positive(\n\t\t\t'positionIntervalMs',\n\t\t\toptions.positionIntervalMs,\n\t\t\t60_000,\n\t\t),\n\t\tflushIntervalMs,\n\t\tpageSize: positive('pageSize', options.pageSize, 100),\n\t\tonHistoryLost: options.onHistoryLost ?? 'reindex',\n\t\ttransform: options.transform as (document: Doc) => unknown,\n\t\ttoIndexId: (options.toIndexId ?? String) as (id: unknown) => unknown,\n\t\tfollower: { running: false },\n\t};\n}\n",
6
+ "import type { ResumeToken } from '@nxgt/mongo';\nimport { MeilisearchApiError } from 'meilisearch';\nimport { deleteIds, sendDocuments } from './batch';\nimport type { Doc, SyncContext } from './context';\nimport { entryOf, keyOf } from './documents';\nimport { failed } from './errors';\nimport { confirmLease, type HeldLease, leaseLost, withLease } from './lease';\nimport { checkIdle } from './running';\nimport { saveState } from './state';\nimport type { ReindexReport } from './types';\n\n/**\n * Where the collection's changes are now, without waiting for one: a new\n * stream's first read answers with its position. The server holds that read\n * for up to `maxAwaitTimeMS` — a second by default, measured.\n */\nasync function currentToken(ctx: SyncContext): Promise<ResumeToken> {\n\tconst stream = ctx.collection.raw.watch([], { maxAwaitTimeMS: 1 });\n\ttry {\n\t\tawait stream.tryNext();\n\t\treturn stream.resumeToken as unknown as ResumeToken;\n\t} finally {\n\t\tawait stream.close();\n\t}\n}\n\n/** Every live document through the transform, a page at a time. */\nasync function sendAll(\n\tctx: SyncContext,\n\twanted: Set<string>,\n\tlease: HeldLease,\n) {\n\tlet indexed = 0;\n\tlet skipped = 0;\n\tlet after: string | null | undefined;\n\tdo {\n\t\tconst page = await ctx.collection.paginateByCursor({\n\t\t\tafter,\n\t\t\tlimit: ctx.pageSize,\n\t\t});\n\t\tconst documents: Doc[] = [];\n\t\tfor (const document of page.items as Doc[]) {\n\t\t\tconst entry = await entryOf(ctx, document._id, document);\n\t\t\tif (entry.kind === 'delete') {\n\t\t\t\tskipped += 1;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\twanted.add(entry.key);\n\t\t\tdocuments.push(entry.document);\n\t\t}\n\t\tawait sendDocuments(ctx, documents);\n\t\tindexed += documents.length;\n\t\t// Found lost by a renewal: stop sending beside whoever holds it now.\n\t\tif (lease.lost) throw leaseLost(ctx);\n\t\tafter = page.nextCursor;\n\t} while (after);\n\treturn { indexed, skipped };\n}\n\nconst LIST_LIMIT = 1000;\n\n/** Nothing was ever written to it: an index that does not exist holds nothing. */\nconst missingIndex = (error: unknown) =>\n\terror instanceof MeilisearchApiError &&\n\terror.cause?.code === 'index_not_found';\n\n/** Takes out what the index holds and the collection no longer gives it. */\nasync function removeUnwanted(\n\tctx: SyncContext,\n\twanted: Set<string>,\n): Promise<number> {\n\tconst unwanted: unknown[] = [];\n\tfor (let offset = 0; ; offset += LIST_LIMIT) {\n\t\tconst page = await ctx.index.raw\n\t\t\t.getDocuments({ fields: [ctx.primaryKey], limit: LIST_LIMIT, offset })\n\t\t\t.catch((error: unknown) => {\n\t\t\t\tif (missingIndex(error)) return undefined;\n\t\t\t\tthrow error;\n\t\t\t});\n\t\tif (!page) return 0;\n\t\tfor (const document of page.results as Doc[]) {\n\t\t\tconst id = document[ctx.primaryKey];\n\t\t\tif (!wanted.has(keyOf(id))) unwanted.push(id);\n\t\t}\n\t\tif (offset + LIST_LIMIT >= page.total) break;\n\t}\n\tawait deleteIds(ctx, unwanted);\n\treturn unwanted.length;\n}\n\n/**\n * A reindex, holding the sync's lease for as long as it runs: a follower in\n * another process would otherwise have what it just indexed removed, and\n * never send it again.\n */\nexport async function reindex(ctx: SyncContext): Promise<ReindexReport> {\n\tcheckIdle(ctx, 'reindex');\n\treturn withLease(ctx, 'reindex', (lease) => reindexHeld(ctx, lease));\n}\n\n/**\n * A reindex by a caller that holds the lease. What removes documents and what\n * records a point each go only once the server confirms the lease is still\n * this one's: after another process took it over, either would undo what\n * that process just did.\n */\nexport async function reindexHeld(\n\tctx: SyncContext,\n\tlease: HeldLease,\n): Promise<ReindexReport> {\n\ttry {\n\t\t// Taken first: a change made while the documents are read is followed\n\t\t// again from here, so it cannot fall between the two.\n\t\tconst token = await currentToken(ctx);\n\t\tconst wanted = new Set<string>();\n\t\tconst { indexed, skipped } = await sendAll(ctx, wanted, lease);\n\t\tawait confirmLease(ctx, lease);\n\t\tconst removed = await removeUnwanted(ctx, wanted);\n\t\tawait confirmLease(ctx, lease);\n\t\tawait saveState(ctx, token, true);\n\t\treturn { indexed, skipped, removed };\n\t} catch (error) {\n\t\tthrow failed(ctx.name, 'reindexing', error);\n\t}\n}\n",
6
7
  "import type { Doc, SyncContext } from './context';\nimport type { Entry } from './documents';\n\n/**\n * How long a write may take to be applied. Meilisearch's own default is\n * five seconds, which a batch on a large index outlasts.\n */\nconst WAIT = { wait: { timeout: 120_000 } } as const;\n\nfunction chunks<T>(items: readonly T[], size: number): T[][] {\n\tconst out: T[][] = [];\n\tfor (let i = 0; i < items.length; i += size) {\n\t\tout.push(items.slice(i, i + size));\n\t}\n\treturn out;\n}\n\n/** Adds documents, `batchSize` at a time, each batch applied before the next. */\nexport async function sendDocuments(\n\tctx: SyncContext,\n\tdocuments: readonly Doc[],\n): Promise<void> {\n\tfor (const batch of chunks(documents, ctx.batchSize)) {\n\t\tawait ctx.index.add(batch, WAIT);\n\t}\n}\n\n/** Deletes documents by id, `batchSize` at a time. */\nexport async function deleteIds(\n\tctx: SyncContext,\n\tids: readonly unknown[],\n): Promise<void> {\n\tfor (const batch of chunks(ids, ctx.batchSize)) {\n\t\tawait ctx.index.delete(batch, WAIT);\n\t}\n}\n\n/**\n * Applies entries: one per id, so adds and deletes touch different\n * documents and their order does not matter.\n */\nexport async function send(\n\tctx: SyncContext,\n\tentries: readonly Entry[],\n): Promise<void> {\n\tconst documents: Doc[] = [];\n\tconst ids: unknown[] = [];\n\tfor (const entry of entries) {\n\t\tif (entry.kind === 'upsert') documents.push(entry.document);\n\t\telse ids.push(entry.id);\n\t}\n\tawait sendDocuments(ctx, documents);\n\tawait deleteIds(ctx, ids);\n}\n",
7
- "/** What stopped a sync, as a string to switch on. */\nexport type SearchSyncErrorCode =\n\t/** The resume point is older than the server's change history. */\n\t| 'HISTORY_LOST'\n\t/** The transform gave a document whose primary key is not its index id. */\n\t| 'ID_MISMATCH'\n\t/** The transform gave something that is neither a document nor `null`. */\n\t| 'NOT_A_DOCUMENT'\n\t/** This sync is already following changes in this process. */\n\t| 'RUNNING'\n\t/** Anything else: the cause says what. */\n\t| 'FAILED';\n\nexport interface SearchSyncErrorOptions {\n\tcode: SearchSyncErrorCode;\n\tsync: string;\n\tcause?: unknown;\n}\n\n/** An error of a sync, with the sync's name and what caused it. */\nexport class SearchSyncError extends Error {\n\toverride name = 'SearchSyncError';\n\treadonly code: SearchSyncErrorCode;\n\t/** The sync's name. */\n\treadonly sync: string;\n\n\tconstructor(message: string, options: SearchSyncErrorOptions) {\n\t\tsuper(\n\t\t\tmessage,\n\t\t\toptions.cause === undefined ? undefined : { cause: options.cause },\n\t\t);\n\t\tthis.code = options.code;\n\t\tthis.sync = options.sync;\n\t}\n}\n\n/** `error` as a `SearchSyncError`, kept as it is when it already is one. */\nexport function failed(sync: string, doing: string, error: unknown) {\n\tif (error instanceof SearchSyncError) return error;\n\tconst reason = error instanceof Error ? error.message : String(error);\n\treturn new SearchSyncError(\n\t\t`Search sync \"${sync}\" failed ${doing}: ${reason}`,\n\t\t{\n\t\t\tcode: 'FAILED',\n\t\t\tsync,\n\t\t\tcause: error,\n\t\t},\n\t);\n}\n",
8
+ "/** What stopped a sync, as a string to switch on. */\nexport type SearchSyncErrorCode =\n\t/** The resume point is older than the server's change history. */\n\t| 'HISTORY_LOST'\n\t/** The transform gave a document whose primary key is not its index id. */\n\t| 'ID_MISMATCH'\n\t/** The transform gave something that is neither a document nor `null`. */\n\t| 'NOT_A_DOCUMENT'\n\t/**\n\t * This sync's name is taken: it is following changes in this process, or\n\t * another process holds its lease.\n\t */\n\t| 'RUNNING'\n\t/**\n\t * A running sync's lease was not renewed in time and is no longer its\n\t * own: it stopped rather than follow beside whoever took the name over.\n\t */\n\t| 'LEASE_LOST'\n\t/** Anything else: the cause says what. */\n\t| 'FAILED';\n\nexport interface SearchSyncErrorOptions {\n\tcode: SearchSyncErrorCode;\n\tsync: string;\n\tcause?: unknown;\n}\n\n/** An error of a sync, with the sync's name and what caused it. */\nexport class SearchSyncError extends Error {\n\toverride name = 'SearchSyncError';\n\treadonly code: SearchSyncErrorCode;\n\t/** The sync's name. */\n\treadonly sync: string;\n\n\tconstructor(message: string, options: SearchSyncErrorOptions) {\n\t\tsuper(\n\t\t\tmessage,\n\t\t\toptions.cause === undefined ? undefined : { cause: options.cause },\n\t\t);\n\t\tthis.code = options.code;\n\t\tthis.sync = options.sync;\n\t}\n}\n\n/** `error` as a `SearchSyncError`, kept as it is when it already is one. */\nexport function failed(sync: string, doing: string, error: unknown) {\n\tif (error instanceof SearchSyncError) return error;\n\tconst reason = error instanceof Error ? error.message : String(error);\n\treturn new SearchSyncError(\n\t\t`Search sync \"${sync}\" failed ${doing}: ${reason}`,\n\t\t{\n\t\t\tcode: 'FAILED',\n\t\t\tsync,\n\t\t\tcause: error,\n\t\t},\n\t);\n}\n",
8
9
  "import type { Doc, SyncContext } from './context';\nimport { SearchSyncError } from './errors';\n\n/** One document's fate in the index: sent, or taken out. */\nexport type Entry =\n\t| { kind: 'upsert'; key: string; document: Doc }\n\t| { kind: 'delete'; key: string; id: unknown };\n\n/** An id as a key: `1` and `'1'` are different ids to Meilisearch. */\nexport const keyOf = (id: unknown) => `${typeof id}:${String(id)}`;\n\nconst isRecord = (value: unknown): value is Doc =>\n\ttypeof value === 'object' && value !== null && !Array.isArray(value);\n\n/** What a transform gave back, for a message: its shape, never its value. */\nfunction describe(value: unknown): string {\n\tif (Array.isArray(value)) return 'an array';\n\tif (value === undefined) return 'undefined';\n\treturn `a ${typeof value}`;\n}\n\n/**\n * What the index should hold for a document of the collection: what the\n * transform makes of it, or nothing when it is gone or kept out.\n */\nexport async function entryOf(\n\tctx: SyncContext,\n\tmongoId: unknown,\n\tdocument: Doc | undefined,\n): Promise<Entry> {\n\tconst id = ctx.toIndexId(mongoId);\n\tconst key = keyOf(id);\n\tif (!document) return { kind: 'delete', key, id };\n\tconst indexed = await ctx.transform(document);\n\tif (indexed === null) return { kind: 'delete', key, id };\n\tif (!isRecord(indexed)) {\n\t\t// A `SearchSyncError` with a code of its own, like the id mismatch\n\t\t// below it: a bare `TypeError` came back through `failed()` as\n\t\t// `FAILED`, the code that means \"anything else\", and named neither the\n\t\t// sync nor the document whose transform did it.\n\t\tthrow new SearchSyncError(\n\t\t\t`Search sync \"${ctx.name}\": transform gave ${describe(indexed)} for ` +\n\t\t\t\t`the document ${String(mongoId)}. It must give a document to ` +\n\t\t\t\t'index, or null to keep it out.',\n\t\t\t{ code: 'NOT_A_DOCUMENT', sync: ctx.name },\n\t\t);\n\t}\n\tconst given = indexed[ctx.primaryKey];\n\tif (keyOf(given) !== key) {\n\t\tthrow new SearchSyncError(\n\t\t\t`Search sync \"${ctx.name}\": transform gave \"${ctx.primaryKey}\" ` +\n\t\t\t\t`${JSON.stringify(given)} for the document ${String(mongoId)}, whose ` +\n\t\t\t\t`index id is ${JSON.stringify(id)}. A document under another id ` +\n\t\t\t\t'could never be taken out of the index again.',\n\t\t\t{ code: 'ID_MISMATCH', sync: ctx.name },\n\t\t);\n\t}\n\treturn { kind: 'upsert', key, document: indexed };\n}\n",
9
- "import type { ResumeToken } from '@nxgt/mongo';\nimport { MeilisearchApiError } from 'meilisearch';\nimport { deleteIds, sendDocuments } from './batch';\nimport type { Doc, SyncContext } from './context';\nimport { entryOf, keyOf } from './documents';\nimport { failed } from './errors';\nimport { checkIdle } from './running';\nimport { saveState } from './state';\nimport type { ReindexReport } from './types';\n\n/**\n * Where the collection's changes are now, without waiting for one: a new\n * stream's first read answers with its position. The server holds that read\n * for up to `maxAwaitTimeMS` — a second by default, measured.\n */\nasync function currentToken(ctx: SyncContext): Promise<ResumeToken> {\n\tconst stream = ctx.collection.raw.watch([], { maxAwaitTimeMS: 1 });\n\ttry {\n\t\tawait stream.tryNext();\n\t\treturn stream.resumeToken as unknown as ResumeToken;\n\t} finally {\n\t\tawait stream.close();\n\t}\n}\n\n/** Every live document through the transform, a page at a time. */\nasync function sendAll(ctx: SyncContext, wanted: Set<string>) {\n\tlet indexed = 0;\n\tlet skipped = 0;\n\tlet after: string | null | undefined;\n\tdo {\n\t\tconst page = await ctx.collection.paginateByCursor({\n\t\t\tafter,\n\t\t\tlimit: ctx.pageSize,\n\t\t});\n\t\tconst documents: Doc[] = [];\n\t\tfor (const document of page.items as Doc[]) {\n\t\t\tconst entry = await entryOf(ctx, document._id, document);\n\t\t\tif (entry.kind === 'delete') {\n\t\t\t\tskipped += 1;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\twanted.add(entry.key);\n\t\t\tdocuments.push(entry.document);\n\t\t}\n\t\tawait sendDocuments(ctx, documents);\n\t\tindexed += documents.length;\n\t\tafter = page.nextCursor;\n\t} while (after);\n\treturn { indexed, skipped };\n}\n\nconst LIST_LIMIT = 1000;\n\n/** Nothing was ever written to it: an index that does not exist holds nothing. */\nconst missingIndex = (error: unknown) =>\n\terror instanceof MeilisearchApiError &&\n\terror.cause?.code === 'index_not_found';\n\n/** Takes out what the index holds and the collection no longer gives it. */\nasync function removeUnwanted(\n\tctx: SyncContext,\n\twanted: Set<string>,\n): Promise<number> {\n\tconst unwanted: unknown[] = [];\n\tfor (let offset = 0; ; offset += LIST_LIMIT) {\n\t\tconst page = await ctx.index.raw\n\t\t\t.getDocuments({ fields: [ctx.primaryKey], limit: LIST_LIMIT, offset })\n\t\t\t.catch((error: unknown) => {\n\t\t\t\tif (missingIndex(error)) return undefined;\n\t\t\t\tthrow error;\n\t\t\t});\n\t\tif (!page) return 0;\n\t\tfor (const document of page.results as Doc[]) {\n\t\t\tconst id = document[ctx.primaryKey];\n\t\t\tif (!wanted.has(keyOf(id))) unwanted.push(id);\n\t\t}\n\t\tif (offset + LIST_LIMIT >= page.total) break;\n\t}\n\tawait deleteIds(ctx, unwanted);\n\treturn unwanted.length;\n}\n\nexport async function reindex(ctx: SyncContext): Promise<ReindexReport> {\n\tcheckIdle(ctx, 'reindex');\n\ttry {\n\t\t// Taken first: a change made while the documents are read is followed\n\t\t// again from here, so it cannot fall between the two.\n\t\tconst token = await currentToken(ctx);\n\t\tconst wanted = new Set<string>();\n\t\tconst { indexed, skipped } = await sendAll(ctx, wanted);\n\t\tconst removed = await removeUnwanted(ctx, wanted);\n\t\tawait saveState(ctx, token, true);\n\t\treturn { indexed, skipped, removed };\n\t} catch (error) {\n\t\tthrow failed(ctx.name, 'reindexing', error);\n\t}\n}\n",
10
+ "import { hostname } from 'node:os';\nimport { ObjectId } from 'mongodb';\nimport type { SyncContext } from './context';\nimport { failed, SearchSyncError } from './errors';\n\n/**\n * The lease on a sync's name: one document in the state collection, whose\n * `_id` is `{ lease: <name> }` — an object, so it can never be a sync's own\n * state, whose `_id` is the name itself.\n *\n * Taking it is one atomic write whose update decides — the server, not a\n * read before a write. Every time is the server's (`$$NOW`), so hosts whose\n * clocks disagree still agree on when a lease lapses.\n *\n * Renewal, release and the holder's name follow `@nxgt/mongo`'s migration\n * lock (`migrations/lock.ts`); taking it does not — see `take`.\n */\nexport interface LeaseDocument {\n\t_id: { lease: string };\n\tholder: string;\n\tacquiredAt: Date;\n\texpiresAt: Date;\n}\n\n/** A held lease: data, and whether a renewal found it gone. */\nexport interface HeldLease {\n\treadonly holder: string;\n\tlost: boolean;\n}\n\nconst idOf = (ctx: SyncContext) => ({ lease: ctx.name });\n\n/**\n * Inserts the lease, or takes over one that lapsed, in one atomic upsert.\n *\n * The filter is `_id` alone — the server allows no `$expr` in an upsert's\n * filter — and the decision is made by the update instead: a pipeline that\n * keeps the current holder's fields while its lease runs, and writes this\n * holder's when there is none or it lapsed. What comes back says who won.\n * It is a `findAndModify`, not an `aggregate`: `@nxgt/mongo`'s migration lock\n * takes its lock with `$documents`/`$merge`, which a failpoint or a monitor\n * aimed at the change stream's `aggregate` would also catch.\n */\nasync function take(ctx: SyncContext, holder: string): Promise<boolean> {\n\tconst free = {\n\t\t$or: [\n\t\t\t{ $eq: [{ $type: '$expiresAt' }, 'missing'] },\n\t\t\t{ $lte: ['$expiresAt', '$$NOW'] },\n\t\t],\n\t};\n\tconst pick = (mine: unknown, theirs: string) => ({\n\t\t$cond: [free, mine, theirs],\n\t});\n\ttry {\n\t\tconst after = await ctx.leases.findOneAndUpdate(\n\t\t\t{ _id: idOf(ctx) },\n\t\t\t[\n\t\t\t\t{\n\t\t\t\t\t$set: {\n\t\t\t\t\t\tholder: pick({ $literal: holder }, '$holder'),\n\t\t\t\t\t\tacquiredAt: pick('$$NOW', '$acquiredAt'),\n\t\t\t\t\t\texpiresAt: pick({ $add: ['$$NOW', ctx.leaseMs] }, '$expiresAt'),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t],\n\t\t\t{ upsert: true, returnDocument: 'after' },\n\t\t);\n\t\treturn after?.holder === holder;\n\t} catch (error) {\n\t\t// Two upserts racing to insert the same `_id`: the other one won.\n\t\tif ((error as { code?: number }).code === 11000) return false;\n\t\tthrow error;\n\t}\n}\n\n/**\n * Takes the lease, or throws `RUNNING` naming who holds it and until when —\n * the same code a second `start` in this process gets, since it means the\n * same thing: this name is being followed.\n */\nexport async function acquire(\n\tctx: SyncContext,\n\tdoing: string,\n): Promise<HeldLease> {\n\tconst holder = `${hostname()}:${process.pid}:${new ObjectId().toHexString()}`;\n\tlet current: LeaseDocument | null = null;\n\ttry {\n\t\t// Twice at most: a holder that let go between the take and the read\n\t\t// left the name free, and refusing it then would be wrong.\n\t\tfor (let attempt = 0; attempt < 2; attempt += 1) {\n\t\t\tif (await take(ctx, holder)) return { holder, lost: false };\n\t\t\tcurrent = await ctx.leases.findOne({ _id: idOf(ctx) });\n\t\t\tif (current) break;\n\t\t}\n\t} catch (error) {\n\t\tthrow failed(ctx.name, 'taking its lease', error);\n\t}\n\tconst until =\n\t\tcurrent?.expiresAt instanceof Date\n\t\t\t? current.expiresAt.toISOString()\n\t\t\t: 'it lets go';\n\tthrow new SearchSyncError(\n\t\t`Search sync \"${ctx.name}\" is held by ${current?.holder ?? 'another process'} ` +\n\t\t\t`until ${until}: ` +\n\t\t\t`wait for it to close, or for its lease to lapse, before you ${doing}.`,\n\t\t{ code: 'RUNNING', sync: ctx.name },\n\t);\n}\n\n/**\n * Pushes the lease's end back by `leaseMs`. A renewal that finds another\n * holder marks the lease lost; one that does not reach the server is tried\n * again at the next beat, and the lease lapses only if none reaches it for a\n * whole `leaseMs`.\n */\nexport async function renew(ctx: SyncContext, lease: HeldLease): Promise<void> {\n\ttry {\n\t\tconst result = await ctx.leases.updateOne(\n\t\t\t{ _id: idOf(ctx), holder: lease.holder },\n\t\t\t[{ $set: { expiresAt: { $add: ['$$NOW', ctx.leaseMs] } } }],\n\t\t);\n\t\tif (result.matchedCount === 0) lease.lost = true;\n\t} catch {\n\t\t// Tried again at the next beat.\n\t}\n}\n\n/**\n * Renews the lease now, and throws `LEASE_LOST` if it is someone else's: the\n * check before a step that must not run beside another holder.\n */\nexport async function confirmLease(\n\tctx: SyncContext,\n\tlease: HeldLease,\n): Promise<void> {\n\tlet result: { matchedCount: number };\n\ttry {\n\t\tresult = await ctx.leases.updateOne(\n\t\t\t{ _id: idOf(ctx), holder: lease.holder },\n\t\t\t[{ $set: { expiresAt: { $add: ['$$NOW', ctx.leaseMs] } } }],\n\t\t);\n\t} catch (error) {\n\t\t// Unlike a timed renewal, not tried again later: what comes next must\n\t\t// not run on a lease nobody could confirm.\n\t\tthrow failed(ctx.name, 'checking its lease', error);\n\t}\n\tif (result.matchedCount === 0) lease.lost = true;\n\tif (lease.lost) throw leaseLost(ctx);\n}\n\n/** Lets go of it, if it is still this holder's. One that fails lapses. */\nexport async function release(\n\tctx: SyncContext,\n\tlease: HeldLease,\n): Promise<void> {\n\tawait ctx.leases\n\t\t.deleteOne({ _id: idOf(ctx), holder: lease.holder })\n\t\t.catch(() => undefined);\n}\n\n/** The error a sync stops with once its lease is someone else's. */\nexport function leaseLost(ctx: SyncContext): SearchSyncError {\n\treturn new SearchSyncError(\n\t\t`Search sync \"${ctx.name}\" lost its lease: another process holds the ` +\n\t\t\t'name now, or the lease was removed (it lapses when not renewed ' +\n\t\t\t`within ${ctx.leaseMs} ms). It stopped rather than run beside it.`,\n\t\t{ code: 'LEASE_LOST', sync: ctx.name },\n\t);\n}\n\n/**\n * Renews the lease every third of `leaseMs`, and calls `onLost` once when a\n * renewal finds it someone else's. Gives back what stops the renewals.\n */\nexport function keepLease(\n\tctx: SyncContext,\n\tlease: HeldLease,\n\tonLost: () => void,\n): () => void {\n\tlet stopped = false;\n\tconst beat = setInterval(\n\t\t() => {\n\t\t\tvoid renew(ctx, lease).then(() => {\n\t\t\t\t// A renewal still in flight when it was stopped reports nothing.\n\t\t\t\tif (lease.lost && !stopped) {\n\t\t\t\t\tclearInterval(beat);\n\t\t\t\t\tonLost();\n\t\t\t\t}\n\t\t\t});\n\t\t},\n\t\tMath.max(1, Math.floor(ctx.leaseMs / 3)),\n\t);\n\treturn () => {\n\t\tstopped = true;\n\t\tclearInterval(beat);\n\t};\n}\n\n/**\n * Runs `fn` holding the lease, renewed every third of `leaseMs`. `fn` checks\n * it is still held where that matters (`confirmLease`).\n */\nexport async function withLease<T>(\n\tctx: SyncContext,\n\tdoing: string,\n\tfn: (lease: HeldLease) => Promise<T>,\n): Promise<T> {\n\tconst lease = await acquire(ctx, doing);\n\tconst stop = keepLease(ctx, lease, () => undefined);\n\ttry {\n\t\treturn await fn(lease);\n\t} finally {\n\t\tstop();\n\t\tawait release(ctx, lease);\n\t}\n}\n",
10
11
  "import type { SyncContext } from './context';\nimport { SearchSyncError } from './errors';\n\n/**\n * Refuses to do beside a running follower what would undo its work: a\n * reindex removes what the follower has just indexed, and the follower,\n * having handled that change, never sends it again.\n */\nexport function checkIdle(ctx: SyncContext, doing: string): void {\n\tif (!ctx.follower.running) return;\n\tthrow new SearchSyncError(\n\t\t`Search sync \"${ctx.name}\" is already following changes in this ` +\n\t\t\t`process: close it before you ${doing}.`,\n\t\t{ code: 'RUNNING', sync: ctx.name },\n\t);\n}\n",
11
12
  "import type { ResumeToken } from '@nxgt/mongo';\nimport type { SyncContext } from './context';\nimport { failed } from './errors';\nimport type { SearchSyncState } from './types';\n\n/** What the sync has recorded, or `undefined` before its first reindex. */\nexport async function readState(\n\tctx: SyncContext,\n): Promise<SearchSyncState | undefined> {\n\ttry {\n\t\treturn (await ctx.state.findOne({ _id: ctx.name })) ?? undefined;\n\t} catch (error) {\n\t\tthrow failed(ctx.name, 'reading its resume point', error);\n\t}\n}\n\n/** Forgets everything recorded: the next `start` reindexes. */\nexport async function clearState(ctx: SyncContext): Promise<void> {\n\ttry {\n\t\tawait ctx.state.deleteOne({ _id: ctx.name });\n\t} catch (error) {\n\t\tthrow failed(ctx.name, 'forgetting its resume point', error);\n\t}\n}\n\n/** Moves the resume point, and says a reindex finished when one did. */\nexport async function saveState(\n\tctx: SyncContext,\n\tresumeToken: ResumeToken,\n\treindexed: boolean,\n): Promise<void> {\n\tconst now = new Date();\n\ttry {\n\t\tawait ctx.state.updateOne(\n\t\t\t{ _id: ctx.name },\n\t\t\t{\n\t\t\t\t$set: {\n\t\t\t\t\tresumeToken,\n\t\t\t\t\tupdatedAt: now,\n\t\t\t\t\t...(reindexed ? { reindexedAt: now } : {}),\n\t\t\t\t},\n\t\t\t},\n\t\t\t{ upsert: true },\n\t\t);\n\t} catch (error) {\n\t\tthrow failed(ctx.name, 'recording its resume point', error);\n\t}\n}\n",
12
- "import type {\n\tAnyCollectionDefinition,\n\tChangeOf,\n\tChangeSubscription,\n\tResumeToken,\n} from '@nxgt/mongo';\nimport { send } from './batch';\nimport type { Doc, SyncContext } from './context';\nimport { type Entry, entryOf } from './documents';\nimport { failed, SearchSyncError } from './errors';\nimport { reindex } from './reindex';\nimport { checkIdle } from './running';\nimport { clearState, readState, saveState } from './state';\nimport type { RunningSearchSync } from './types';\n\n/** The server no longer has the history a token points into. */\nfunction historyLost(error: unknown): boolean {\n\tconst code = (error as { serverCode?: number } | undefined)?.serverCode;\n\treturn code === 286 || code === 280;\n}\n\n/** What a running sync holds between changes. */\ninterface Following {\n\treadonly pending: Map<string, Entry>;\n\t/** The token of the last change in `pending`. */\n\tlast: ResumeToken | undefined;\n\tsaved: ResumeToken;\n\t/** Changes whose handler has not queued them yet. */\n\tinFlight: number;\n\ttimer: ReturnType<typeof setTimeout> | undefined;\n\t/** Records where the stream is while nothing is being sent. */\n\tbeat: ReturnType<typeof setInterval> | undefined;\n\t/** Flushes run one after the other. */\n\tchain: Promise<void>;\n\t/** The stream has stopped: what is queued no longer records anything. */\n\tdone: boolean;\n\tfailure: SearchSyncError | undefined;\n}\n\n/**\n * Sends what is pending, then records the token that covers it — or, with\n * nothing to send and no change being handled, where the stream is: a quiet\n * collection would otherwise keep a resume point until the server's history\n * no longer reaches it.\n */\nfunction flush(\n\tctx: SyncContext,\n\tf: Following,\n\tsubscription: ChangeSubscription,\n): Promise<void> {\n\tf.chain = f.chain.then(async () => {\n\t\tif (f.failure) throw f.failure;\n\t\tif (f.done) return;\n\t\tclearTimeout(f.timer);\n\t\tf.timer = undefined;\n\t\tconst entries = [...f.pending.values()];\n\t\tconst upTo = f.last;\n\t\tf.pending.clear();\n\t\ttry {\n\t\t\tif (entries.length > 0) await send(ctx, entries);\n\t\t\tconst idle = entries.length === 0 && f.inFlight === 0;\n\t\t\tconst at = upTo ?? (idle ? subscription.position : undefined);\n\t\t\tif (at && at !== f.saved) {\n\t\t\t\tawait saveState(ctx, at, false);\n\t\t\t\tf.saved = at;\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tf.failure ??= failed(ctx.name, 'sending changes', error);\n\t\t\tthrow f.failure;\n\t\t}\n\t});\n\treturn f.chain;\n}\n\nasync function open(\n\tctx: SyncContext,\n\ttoken: ResumeToken,\n): Promise<RunningSearchSync> {\n\tconst f: Following = {\n\t\tpending: new Map(),\n\t\tlast: undefined,\n\t\tsaved: token,\n\t\tinFlight: 0,\n\t\ttimer: undefined,\n\t\tbeat: undefined,\n\t\tchain: Promise.resolve(),\n\t\tdone: false,\n\t\tfailure: undefined,\n\t};\n\tconst handle = async (change: ChangeOf<AnyCollectionDefinition>) => {\n\t\tif (f.failure) throw f.failure;\n\t\tf.inFlight += 1;\n\t\tlet entry: Entry;\n\t\ttry {\n\t\t\tconst document =\n\t\t\t\tchange.type === 'delete' ? undefined : (change.document as Doc);\n\t\t\tentry = await entryOf(ctx, change.id, document);\n\t\t} catch (error) {\n\t\t\t// Kept, so `flush` and `close` report it as `closed` does.\n\t\t\tf.failure = failed(ctx.name, 'following changes', error);\n\t\t\tthrow f.failure;\n\t\t} finally {\n\t\t\tf.inFlight -= 1;\n\t\t}\n\t\tf.pending.set(entry.key, entry);\n\t\tf.last = change.resumeToken;\n\t\tif (f.pending.size >= ctx.batchSize) {\n\t\t\tawait flush(ctx, f, subscription);\n\t\t} else if (!f.timer) {\n\t\t\tf.timer = setTimeout(() => {\n\t\t\t\tflush(ctx, f, subscription).catch(() => subscription.close());\n\t\t\t}, ctx.flushIntervalMs);\n\t\t}\n\t};\n\tconst subscription = ctx.collection.onChange(handle, { startAfter: token });\n\tf.beat = setInterval(() => {\n\t\tflush(ctx, f, subscription).catch(() => subscription.close());\n\t}, ctx.positionIntervalMs);\n\tconst stop = () => {\n\t\tf.done = true;\n\t\tclearTimeout(f.timer);\n\t\tclearInterval(f.beat);\n\t\tctx.follower.running = false;\n\t};\n\tconst closed = subscription.closed.then(\n\t\tasync (reason) => {\n\t\t\tstop();\n\t\t\t// A flush the timer or the beat had queued would otherwise record a\n\t\t\t// point after this.\n\t\t\tawait f.chain.catch(() => undefined);\n\t\t\tif (f.failure) throw f.failure;\n\t\t\t// The collection is gone, and so is the point the stream stopped at:\n\t\t\t// what is recorded is older, and every later start would stop here\n\t\t\t// again. Forgetting it makes the next one reindex.\n\t\t\tif (reason === 'invalidated') await clearState(ctx);\n\t\t\treturn reason;\n\t\t},\n\t\t(error: unknown) => {\n\t\t\tstop();\n\t\t\tthrow f.failure ?? failed(ctx.name, 'following changes', error);\n\t\t},\n\t);\n\ttry {\n\t\tawait subscription.ready;\n\t} catch (error) {\n\t\tstop();\n\t\tclosed.catch(() => undefined);\n\t\tthrow error;\n\t}\n\tctx.follower.running = true;\n\tconst close = async () => {\n\t\ttry {\n\t\t\tawait flush(ctx, f, subscription);\n\t\t} finally {\n\t\t\tawait subscription.close();\n\t\t}\n\t};\n\treturn {\n\t\tready: subscription.ready,\n\t\tclosed,\n\t\tflush: () => flush(ctx, f, subscription),\n\t\tclose,\n\t\t[Symbol.asyncDispose]: close,\n\t};\n}\n\n/**\n * Follows the collection from its recorded point — after a reindex when\n * nothing is recorded, or when the point is older than the server's history\n * and `onHistoryLost` allows it.\n */\nexport async function start(ctx: SyncContext): Promise<RunningSearchSync> {\n\tcheckIdle(ctx, 'start it twice');\n\tlet state = await readState(ctx);\n\tif (!state) {\n\t\tawait reindex(ctx);\n\t\tstate = await readState(ctx);\n\t}\n\ttry {\n\t\treturn await open(ctx, (state as { resumeToken: ResumeToken }).resumeToken);\n\t} catch (error) {\n\t\tif (!historyLost(error)) throw failed(ctx.name, 'starting', error);\n\t\tif (ctx.onHistoryLost === 'fail') {\n\t\t\tthrow new SearchSyncError(\n\t\t\t\t`Search sync \"${ctx.name}\" was last at a point the server's change ` +\n\t\t\t\t\t'history no longer reaches. Reindex it, or start it with ' +\n\t\t\t\t\t\"onHistoryLost: 'reindex'.\",\n\t\t\t\t{ code: 'HISTORY_LOST', sync: ctx.name, cause: error },\n\t\t\t);\n\t\t}\n\t\tawait reindex(ctx);\n\t\tconst fresh = (await readState(ctx)) as { resumeToken: ResumeToken };\n\t\ttry {\n\t\t\treturn await open(ctx, fresh.resumeToken);\n\t\t} catch (again) {\n\t\t\tthrow failed(ctx.name, 'starting', again);\n\t\t}\n\t}\n}\n",
13
- "import type { AnyIndexDefinition } from '@nxgt/meilisearch';\nimport type { AnyCollectionDefinition } from '@nxgt/mongo';\nimport { createContext } from './context';\nimport { start } from './follow';\nimport { reindex } from './reindex';\nimport { readState } from './state';\nimport type { SearchSync, SearchSyncOptions } from './types';\n\n/**\n * Keeps an index in step with a collection.\n *\n * ```ts\n * const usersSearch = createSearchSync({\n * \tcollection: getCollection(db, users),\n * \tindex: bindIndex(meili, userIndex),\n * \ttransform: (user) => (user.active ? { id: user.id, name: user.name } : null),\n * });\n * const running = await usersSearch.start(); // reindexes the first time\n * // …\n * await running.close();\n * ```\n *\n * Nothing is sent before `reindex` or `start`.\n */\nexport function createSearchSync<\n\tC extends AnyCollectionDefinition,\n\tI extends AnyIndexDefinition,\n>(options: SearchSyncOptions<C, I>): SearchSync {\n\t// The one place the caller's types are let go: a method taking `IdOf<C>`\n\t// is not one taking any id, so the widening cannot be implicit. Below\n\t// here, documents are records and ids are values.\n\tconst ctx = createContext(\n\t\toptions as unknown as SearchSyncOptions<\n\t\t\tAnyCollectionDefinition,\n\t\t\tAnyIndexDefinition\n\t\t>,\n\t);\n\treturn {\n\t\tname: ctx.name,\n\t\treindex: () => reindex(ctx),\n\t\tstart: () => start(ctx),\n\t\tstate: () => readState(ctx),\n\t};\n}\n"
13
+ "import type {\n\tAnyCollectionDefinition,\n\tChangeOf,\n\tChangeSubscription,\n\tCloseReason,\n\tResumeToken,\n} from '@nxgt/mongo';\nimport { send } from './batch';\nimport type { Doc, SyncContext } from './context';\nimport { type Entry, entryOf } from './documents';\nimport { failed, type SearchSyncError } from './errors';\nimport { type HeldLease, keepLease, leaseLost, release } from './lease';\nimport { clearState, saveState } from './state';\nimport type { RunningSearchSync } from './types';\n\nconst noop = () => undefined;\n\n/** What a running sync holds between changes. */\ninterface Following {\n\treadonly pending: Map<string, Entry>;\n\t/** The token of the last change in `pending`. */\n\tlast: ResumeToken | undefined;\n\tsaved: ResumeToken;\n\t/** Changes whose handler has not queued them yet. */\n\tinFlight: number;\n\ttimer: ReturnType<typeof setTimeout> | undefined;\n\t/** Records where the stream is while nothing is being sent. */\n\tbeat: ReturnType<typeof setInterval> | undefined;\n\t/** Stops renewing the lease on the sync's name. */\n\tstopLease: () => void;\n\t/**\n\t * The stream was heard. Only then does `closed` let go of the lease: a\n\t * stream that never got ready leaves it to `start`, which may reindex and\n\t * open again on it after a lost history.\n\t */\n\theard: boolean;\n\t/** Flushes run one after the other. */\n\tchain: Promise<void>;\n\t/** The stream has stopped: what is queued no longer records anything. */\n\tdone: boolean;\n\tfailure: SearchSyncError | undefined;\n}\n\n/**\n * Sends what is pending, then records the token that covers it — or, with\n * nothing to send and no change being handled, where the stream is: a quiet\n * collection would otherwise keep a resume point until the server's history\n * no longer reaches it.\n */\nfunction flush(\n\tctx: SyncContext,\n\tf: Following,\n\tsubscription: ChangeSubscription,\n): Promise<void> {\n\tf.chain = f.chain.then(async () => {\n\t\tif (f.failure) throw f.failure;\n\t\tif (f.done) return;\n\t\tclearTimeout(f.timer);\n\t\tf.timer = undefined;\n\t\tconst entries = [...f.pending.values()];\n\t\tconst upTo = f.last;\n\t\tf.pending.clear();\n\t\ttry {\n\t\t\tif (entries.length > 0) await send(ctx, entries);\n\t\t\tconst idle = entries.length === 0 && f.inFlight === 0;\n\t\t\tconst at = upTo ?? (idle ? subscription.position : undefined);\n\t\t\tif (at && at !== f.saved) {\n\t\t\t\tawait saveState(ctx, at, false);\n\t\t\t\tf.saved = at;\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tf.failure ??= failed(ctx.name, 'sending changes', error);\n\t\t\tthrow f.failure;\n\t\t}\n\t});\n\treturn f.chain;\n}\n\n/** A sync that has not heard anything yet, from `token`. */\nfunction following(token: ResumeToken): Following {\n\treturn {\n\t\tpending: new Map(),\n\t\tlast: undefined,\n\t\tsaved: token,\n\t\tinFlight: 0,\n\t\ttimer: undefined,\n\t\tbeat: undefined,\n\t\tstopLease: noop,\n\t\theard: false,\n\t\tchain: Promise.resolve(),\n\t\tdone: false,\n\t\tfailure: undefined,\n\t};\n}\n\n/** Stops everything a running sync does on its own. */\nfunction stopFollowing(ctx: SyncContext, f: Following): void {\n\tf.done = true;\n\tclearTimeout(f.timer);\n\tclearInterval(f.beat);\n\tf.stopLease();\n\tctx.follower.running = false;\n}\n\n/**\n * What `closed` settles to once the stream stops: the reason, or the failure\n * that stopped it — after the last queued flush, and with the lease let go.\n */\nfunction settle(\n\tctx: SyncContext,\n\tf: Following,\n\tsubscription: ChangeSubscription,\n\tlease: HeldLease,\n): Promise<CloseReason> {\n\tconst letGo = () => (f.heard ? release(ctx, lease) : Promise.resolve());\n\treturn subscription.closed.then(\n\t\tasync (reason) => {\n\t\t\tstopFollowing(ctx, f);\n\t\t\ttry {\n\t\t\t\t// A flush the timer or the beat had queued would otherwise record\n\t\t\t\t// a point after this.\n\t\t\t\tawait f.chain.catch(noop);\n\t\t\t\tif (f.failure) throw f.failure;\n\t\t\t\t// The collection is gone, and so is the point the stream stopped\n\t\t\t\t// at: what is recorded is older, and every later start would stop\n\t\t\t\t// here again. Forgetting it makes the next one reindex.\n\t\t\t\tif (reason === 'invalidated') await clearState(ctx);\n\t\t\t\treturn reason;\n\t\t\t} finally {\n\t\t\t\tawait letGo();\n\t\t\t}\n\t\t},\n\t\tasync (error: unknown) => {\n\t\t\tstopFollowing(ctx, f);\n\t\t\tawait letGo();\n\t\t\tthrow f.failure ?? failed(ctx.name, 'following changes', error);\n\t\t},\n\t);\n}\n\nexport async function open(\n\tctx: SyncContext,\n\ttoken: ResumeToken,\n\tlease: HeldLease,\n): Promise<RunningSearchSync> {\n\tconst f = following(token);\n\tconst handle = async (change: ChangeOf<AnyCollectionDefinition>) => {\n\t\tif (f.failure) throw f.failure;\n\t\tf.inFlight += 1;\n\t\tlet entry: Entry;\n\t\ttry {\n\t\t\tconst document =\n\t\t\t\tchange.type === 'delete' ? undefined : (change.document as Doc);\n\t\t\tentry = await entryOf(ctx, change.id, document);\n\t\t} catch (error) {\n\t\t\t// Kept, so `flush` and `close` report it as `closed` does.\n\t\t\tf.failure = failed(ctx.name, 'following changes', error);\n\t\t\tthrow f.failure;\n\t\t} finally {\n\t\t\tf.inFlight -= 1;\n\t\t}\n\t\tf.pending.set(entry.key, entry);\n\t\tf.last = change.resumeToken;\n\t\tif (f.pending.size >= ctx.batchSize) {\n\t\t\tawait flush(ctx, f, subscription);\n\t\t} else if (!f.timer) {\n\t\t\tf.timer = setTimeout(() => {\n\t\t\t\tflush(ctx, f, subscription).catch(() => subscription.close());\n\t\t\t}, ctx.flushIntervalMs);\n\t\t}\n\t};\n\tconst subscription = ctx.collection.onChange(handle, { startAfter: token });\n\tf.beat = setInterval(() => {\n\t\tflush(ctx, f, subscription).catch(() => subscription.close());\n\t}, ctx.positionIntervalMs);\n\t// A lease found gone means another process may be following this name:\n\t// stop at once, before anything else is sent or recorded beside it.\n\tf.stopLease = keepLease(ctx, lease, () => {\n\t\tif (f.failure) return;\n\t\tf.failure = leaseLost(ctx);\n\t\tvoid subscription.close();\n\t});\n\tconst closed = settle(ctx, f, subscription, lease);\n\ttry {\n\t\tawait subscription.ready;\n\t} catch (error) {\n\t\tstopFollowing(ctx, f);\n\t\tclosed.catch(noop);\n\t\tthrow error;\n\t}\n\tf.heard = true;\n\tctx.follower.running = true;\n\tconst close = async () => {\n\t\ttry {\n\t\t\tawait flush(ctx, f, subscription);\n\t\t} finally {\n\t\t\tawait subscription.close();\n\t\t\t// The lease is let go in `closed`'s handler: waited for, so a\n\t\t\t// `reindex` or a `start` right after this finds the name free. Not\n\t\t\t// from inside a change being handled, which `closed` waits for.\n\t\t\tif (f.inFlight === 0) await closed.then(noop, noop);\n\t\t}\n\t};\n\treturn {\n\t\tready: subscription.ready,\n\t\tclosed,\n\t\tflush: () => flush(ctx, f, subscription),\n\t\tclose,\n\t\t[Symbol.asyncDispose]: close,\n\t};\n}\n",
14
+ "import type { ResumeToken } from '@nxgt/mongo';\nimport type { SyncContext } from './context';\nimport { failed, SearchSyncError } from './errors';\nimport { open } from './follow';\nimport {\n\tacquire,\n\tconfirmLease,\n\ttype HeldLease,\n\tkeepLease,\n\trelease,\n} from './lease';\nimport { reindexHeld } from './reindex';\nimport { checkIdle } from './running';\nimport { readState } from './state';\nimport type { RunningSearchSync } from './types';\n\n/** The server no longer has the history a token points into. */\nfunction historyLost(error: unknown): boolean {\n\tconst code = (error as { serverCode?: number } | undefined)?.serverCode;\n\treturn code === 286 || code === 280;\n}\n\n/**\n * Follows the collection from its recorded point — after a reindex when\n * nothing is recorded, or when the point is older than the server's history\n * and `onHistoryLost` allows it.\n */\nexport async function start(ctx: SyncContext): Promise<RunningSearchSync> {\n\tcheckIdle(ctx, 'start it twice');\n\t// Before anything else, the first reindex included: two processes\n\t// starting at once would otherwise both reindex, and both follow.\n\tconst lease = await acquire(ctx, 'start it');\n\t// Renewed while it reindexes, which can outlast `leaseMs`; the follower\n\t// renews it on its own once it is open.\n\tconst stop = keepLease(ctx, lease, () => undefined);\n\ttry {\n\t\tconst running = await startHeld(ctx, lease);\n\t\tstop();\n\t\treturn running;\n\t} catch (error) {\n\t\tstop();\n\t\tawait release(ctx, lease);\n\t\tthrow error;\n\t}\n}\n\n/** Opens the follower, once the server confirms the lease is still held. */\nasync function openHeld(\n\tctx: SyncContext,\n\ttoken: ResumeToken,\n\tlease: HeldLease,\n): Promise<RunningSearchSync> {\n\tawait confirmLease(ctx, lease);\n\treturn open(ctx, token, lease);\n}\n\nasync function startHeld(\n\tctx: SyncContext,\n\tlease: HeldLease,\n): Promise<RunningSearchSync> {\n\tlet state = await readState(ctx);\n\tif (!state) {\n\t\tawait reindexHeld(ctx, lease);\n\t\tstate = await readState(ctx);\n\t}\n\ttry {\n\t\treturn await openHeld(\n\t\t\tctx,\n\t\t\t(state as { resumeToken: ResumeToken }).resumeToken,\n\t\t\tlease,\n\t\t);\n\t} catch (error) {\n\t\tif (!historyLost(error)) throw failed(ctx.name, 'starting', error);\n\t\tif (ctx.onHistoryLost === 'fail') {\n\t\t\tthrow new SearchSyncError(\n\t\t\t\t`Search sync \"${ctx.name}\" was last at a point the server's change ` +\n\t\t\t\t\t'history no longer reaches. Reindex it, or start it with ' +\n\t\t\t\t\t\"onHistoryLost: 'reindex'.\",\n\t\t\t\t{ code: 'HISTORY_LOST', sync: ctx.name, cause: error },\n\t\t\t);\n\t\t}\n\t\tawait reindexHeld(ctx, lease);\n\t\tconst fresh = (await readState(ctx)) as { resumeToken: ResumeToken };\n\t\ttry {\n\t\t\treturn await openHeld(ctx, fresh.resumeToken, lease);\n\t\t} catch (again) {\n\t\t\tthrow failed(ctx.name, 'starting', again);\n\t\t}\n\t}\n}\n",
15
+ "import type { AnyIndexDefinition } from '@nxgt/meilisearch';\nimport type { AnyCollectionDefinition } from '@nxgt/mongo';\nimport { createContext } from './context';\nimport { reindex } from './reindex';\nimport { start } from './start';\nimport { readState } from './state';\nimport type { SearchSync, SearchSyncOptions } from './types';\n\n/**\n * Keeps an index in step with a collection.\n *\n * ```ts\n * const usersSearch = createSearchSync({\n * \tcollection: getCollection(db, users),\n * \tindex: bindIndex(meili, userIndex),\n * \ttransform: (user) => (user.active ? { id: user.id, name: user.name } : null),\n * });\n * const running = await usersSearch.start(); // reindexes the first time\n * // …\n * await running.close();\n * ```\n *\n * Nothing is sent before `reindex` or `start`.\n */\nexport function createSearchSync<\n\tC extends AnyCollectionDefinition,\n\tI extends AnyIndexDefinition,\n>(options: SearchSyncOptions<C, I>): SearchSync {\n\t// The one place the caller's types are let go: a method taking `IdOf<C>`\n\t// is not one taking any id, so the widening cannot be implicit. Below\n\t// here, documents are records and ids are values.\n\tconst ctx = createContext(\n\t\toptions as unknown as SearchSyncOptions<\n\t\t\tAnyCollectionDefinition,\n\t\t\tAnyIndexDefinition\n\t\t>,\n\t);\n\treturn {\n\t\tname: ctx.name,\n\t\treindex: () => reindex(ctx),\n\t\tstart: () => start(ctx),\n\t\tstate: () => readState(ctx),\n\t};\n}\n"
14
16
  ],
15
- "mappings": "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",
16
- "debugId": "1220BC8109B997D064756E2164756E21",
17
+ "mappings": "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",
18
+ "debugId": "663F5145BB58E86F64756E2164756E21",
17
19
  "names": []
18
20
  }
@@ -0,0 +1,60 @@
1
+ import type { SyncContext } from './context';
2
+ import { SearchSyncError } from './errors';
3
+ /**
4
+ * The lease on a sync's name: one document in the state collection, whose
5
+ * `_id` is `{ lease: <name> }` — an object, so it can never be a sync's own
6
+ * state, whose `_id` is the name itself.
7
+ *
8
+ * Taking it is one atomic write whose update decides — the server, not a
9
+ * read before a write. Every time is the server's (`$$NOW`), so hosts whose
10
+ * clocks disagree still agree on when a lease lapses.
11
+ *
12
+ * Renewal, release and the holder's name follow `@nxgt/mongo`'s migration
13
+ * lock (`migrations/lock.ts`); taking it does not — see `take`.
14
+ */
15
+ export interface LeaseDocument {
16
+ _id: {
17
+ lease: string;
18
+ };
19
+ holder: string;
20
+ acquiredAt: Date;
21
+ expiresAt: Date;
22
+ }
23
+ /** A held lease: data, and whether a renewal found it gone. */
24
+ export interface HeldLease {
25
+ readonly holder: string;
26
+ lost: boolean;
27
+ }
28
+ /**
29
+ * Takes the lease, or throws `RUNNING` naming who holds it and until when —
30
+ * the same code a second `start` in this process gets, since it means the
31
+ * same thing: this name is being followed.
32
+ */
33
+ export declare function acquire(ctx: SyncContext, doing: string): Promise<HeldLease>;
34
+ /**
35
+ * Pushes the lease's end back by `leaseMs`. A renewal that finds another
36
+ * holder marks the lease lost; one that does not reach the server is tried
37
+ * again at the next beat, and the lease lapses only if none reaches it for a
38
+ * whole `leaseMs`.
39
+ */
40
+ export declare function renew(ctx: SyncContext, lease: HeldLease): Promise<void>;
41
+ /**
42
+ * Renews the lease now, and throws `LEASE_LOST` if it is someone else's: the
43
+ * check before a step that must not run beside another holder.
44
+ */
45
+ export declare function confirmLease(ctx: SyncContext, lease: HeldLease): Promise<void>;
46
+ /** Lets go of it, if it is still this holder's. One that fails lapses. */
47
+ export declare function release(ctx: SyncContext, lease: HeldLease): Promise<void>;
48
+ /** The error a sync stops with once its lease is someone else's. */
49
+ export declare function leaseLost(ctx: SyncContext): SearchSyncError;
50
+ /**
51
+ * Renews the lease every third of `leaseMs`, and calls `onLost` once when a
52
+ * renewal finds it someone else's. Gives back what stops the renewals.
53
+ */
54
+ export declare function keepLease(ctx: SyncContext, lease: HeldLease, onLost: () => void): () => void;
55
+ /**
56
+ * Runs `fn` holding the lease, renewed every third of `leaseMs`. `fn` checks
57
+ * it is still held where that matters (`confirmLease`).
58
+ */
59
+ export declare function withLease<T>(ctx: SyncContext, doing: string, fn: (lease: HeldLease) => Promise<T>): Promise<T>;
60
+ //# sourceMappingURL=lease.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lease.d.ts","sourceRoot":"","sources":["../src/lease.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,EAAU,eAAe,EAAE,MAAM,UAAU,CAAC;AAEnD;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,aAAa;IAC7B,GAAG,EAAE;QAAE,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,IAAI,CAAC;IACjB,SAAS,EAAE,IAAI,CAAC;CAChB;AAED,+DAA+D;AAC/D,MAAM,WAAW,SAAS;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,IAAI,EAAE,OAAO,CAAC;CACd;AA+CD;;;;GAIG;AACH,wBAAsB,OAAO,CAC5B,GAAG,EAAE,WAAW,EAChB,KAAK,EAAE,MAAM,GACX,OAAO,CAAC,SAAS,CAAC,CAwBpB;AAED;;;;;GAKG;AACH,wBAAsB,KAAK,CAAC,GAAG,EAAE,WAAW,EAAE,KAAK,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAU7E;AAED;;;GAGG;AACH,wBAAsB,YAAY,CACjC,GAAG,EAAE,WAAW,EAChB,KAAK,EAAE,SAAS,GACd,OAAO,CAAC,IAAI,CAAC,CAcf;AAED,0EAA0E;AAC1E,wBAAsB,OAAO,CAC5B,GAAG,EAAE,WAAW,EAChB,KAAK,EAAE,SAAS,GACd,OAAO,CAAC,IAAI,CAAC,CAIf;AAED,oEAAoE;AACpE,wBAAgB,SAAS,CAAC,GAAG,EAAE,WAAW,GAAG,eAAe,CAO3D;AAED;;;GAGG;AACH,wBAAgB,SAAS,CACxB,GAAG,EAAE,WAAW,EAChB,KAAK,EAAE,SAAS,EAChB,MAAM,EAAE,MAAM,IAAI,GAChB,MAAM,IAAI,CAkBZ;AAED;;;GAGG;AACH,wBAAsB,SAAS,CAAC,CAAC,EAChC,GAAG,EAAE,WAAW,EAChB,KAAK,EAAE,MAAM,EACb,EAAE,EAAE,CAAC,KAAK,EAAE,SAAS,KAAK,OAAO,CAAC,CAAC,CAAC,GAClC,OAAO,CAAC,CAAC,CAAC,CASZ"}
package/dist/reindex.d.ts CHANGED
@@ -1,4 +1,17 @@
1
1
  import type { SyncContext } from './context';
2
+ import { type HeldLease } from './lease';
2
3
  import type { ReindexReport } from './types';
4
+ /**
5
+ * A reindex, holding the sync's lease for as long as it runs: a follower in
6
+ * another process would otherwise have what it just indexed removed, and
7
+ * never send it again.
8
+ */
3
9
  export declare function reindex(ctx: SyncContext): Promise<ReindexReport>;
10
+ /**
11
+ * A reindex by a caller that holds the lease. What removes documents and what
12
+ * records a point each go only once the server confirms the lease is still
13
+ * this one's: after another process took it over, either would undo what
14
+ * that process just did.
15
+ */
16
+ export declare function reindexHeld(ctx: SyncContext, lease: HeldLease): Promise<ReindexReport>;
4
17
  //# sourceMappingURL=reindex.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"reindex.d.ts","sourceRoot":"","sources":["../src/reindex.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAO,WAAW,EAAE,MAAM,WAAW,CAAC;AAKlD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AA2E7C,wBAAsB,OAAO,CAAC,GAAG,EAAE,WAAW,GAAG,OAAO,CAAC,aAAa,CAAC,CActE"}
1
+ {"version":3,"file":"reindex.d.ts","sourceRoot":"","sources":["../src/reindex.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAO,WAAW,EAAE,MAAM,WAAW,CAAC;AAGlD,OAAO,EAAgB,KAAK,SAAS,EAAwB,MAAM,SAAS,CAAC;AAG7E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAiF7C;;;;GAIG;AACH,wBAAsB,OAAO,CAAC,GAAG,EAAE,WAAW,GAAG,OAAO,CAAC,aAAa,CAAC,CAGtE;AAED;;;;;GAKG;AACH,wBAAsB,WAAW,CAChC,GAAG,EAAE,WAAW,EAChB,KAAK,EAAE,SAAS,GACd,OAAO,CAAC,aAAa,CAAC,CAexB"}
@@ -0,0 +1,9 @@
1
+ import type { SyncContext } from './context';
2
+ import type { RunningSearchSync } from './types';
3
+ /**
4
+ * Follows the collection from its recorded point — after a reindex when
5
+ * nothing is recorded, or when the point is older than the server's history
6
+ * and `onHistoryLost` allows it.
7
+ */
8
+ export declare function start(ctx: SyncContext): Promise<RunningSearchSync>;
9
+ //# sourceMappingURL=start.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"start.d.ts","sourceRoot":"","sources":["../src/start.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAa7C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAC;AAQjD;;;;GAIG;AACH,wBAAsB,KAAK,CAAC,GAAG,EAAE,WAAW,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAiBxE"}
package/dist/types.d.ts CHANGED
@@ -48,6 +48,14 @@ export type SearchSyncOptions<C extends AnyCollectionDefinition, I extends AnyIn
48
48
  * none while nothing moves at all.
49
49
  */
50
50
  positionIntervalMs?: number;
51
+ /**
52
+ * How long, in ms, the lease on this sync's name lasts without being
53
+ * renewed. Default `30000`. A running sync — and a `reindex` — holds it,
54
+ * renewed every third of it, so a second process refuses to follow the
55
+ * same name beside it; a process that dies leaves the name free once its
56
+ * lease lapses, and not before.
57
+ */
58
+ leaseMs?: number;
51
59
  /**
52
60
  * How many documents a reindex reads per page; above the collection's
53
61
  * `maxPageSize`, lowered to it. Default `100`.
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACX,kBAAkB,EAClB,UAAU,IAAI,eAAe,EAC7B,IAAI,IAAI,SAAS,EACjB,UAAU,EACV,MAAM,mBAAmB,CAAC;AAC3B,OAAO,KAAK,EACX,uBAAuB,EACvB,WAAW,EACX,IAAI,IAAI,cAAc,EACtB,cAAc,EACd,WAAW,EACX,eAAe,EACf,MAAM,aAAa,CAAC;AAErB;;;GAGG;AACH,MAAM,MAAM,SAAS,CACpB,CAAC,SAAS,uBAAuB,EACjC,CAAC,SAAS,kBAAkB,IACzB,CACH,QAAQ,EAAE,cAAc,CAAC,CAAC,CAAC,KACvB,eAAe,CAAC,CAAC,CAAC,GAAG,IAAI,GAAG,OAAO,CAAC,eAAe,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;AAEpE,4CAA4C;AAC5C,MAAM,MAAM,SAAS,CACpB,CAAC,SAAS,uBAAuB,EACjC,CAAC,SAAS,kBAAkB,IACzB,CAAC,EAAE,EAAE,cAAc,CAAC,CAAC,CAAC,KAAK,SAAS,CAAC,CAAC,CAAC,CAAC;AAE5C;;;GAGG;AACH,KAAK,QAAQ,CAAC,CAAC,SAAS,uBAAuB,EAAE,CAAC,SAAS,kBAAkB,IAC5E,MAAM,SAAS,SAAS,CAAC,CAAC,CAAC,GACxB;IACA,yDAAyD;IACzD,SAAS,CAAC,EAAE,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;CAC5B,GACA;IACA,mEAAmE;IACnE,SAAS,EAAE,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;CAC3B,CAAC;AAEL,MAAM,MAAM,iBAAiB,CAC5B,CAAC,SAAS,uBAAuB,EACjC,CAAC,SAAS,kBAAkB,IACzB;IACH,kEAAkE;IAClE,UAAU,EAAE,eAAe,CAAC,CAAC,CAAC,CAAC;IAC/B,gDAAgD;IAChD,KAAK,EAAE,UAAU,CAAC,CAAC,CAAC,CAAC;IACrB;;;;OAIG;IACH,SAAS,EAAE,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAC3B;;;;OAIG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,kGAAkG;IAClG,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,uEAAuE;IACvE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,mFAAmF;IACnF,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;;;OAKG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,aAAa,CAAC,EAAE,SAAS,GAAG,MAAM,CAAC;CACnC,GAAG,QAAQ,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;AAEnB,gCAAgC;AAChC,MAAM,WAAW,eAAe;IAC/B,uBAAuB;IACvB,GAAG,EAAE,MAAM,CAAC;IACZ,+BAA+B;IAC/B,WAAW,EAAE,WAAW,CAAC;IACzB,wCAAwC;IACxC,SAAS,EAAE,IAAI,CAAC;IAChB,2CAA2C;IAC3C,WAAW,EAAE,IAAI,GAAG,SAAS,CAAC;CAC9B;AAED,MAAM,WAAW,aAAa;IAC7B,mCAAmC;IACnC,OAAO,EAAE,MAAM,CAAC;IAChB,wCAAwC;IACxC,OAAO,EAAE,MAAM,CAAC;IAChB,uEAAuE;IACvE,OAAO,EAAE,MAAM,CAAC;CAChB;AAED,+CAA+C;AAC/C,MAAM,WAAW,iBAAkB,SAAQ,eAAe;IACzD,6CAA6C;IAC7C,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9B;;;;OAIG;IACH,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC,WAAW,CAAC,CAAC;IACtC,yDAAyD;IACzD,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,yCAAyC;IACzC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACvB;AAED,MAAM,WAAW,UAAU;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB;;;;OAIG;IACH,OAAO,IAAI,OAAO,CAAC,aAAa,CAAC,CAAC;IAClC;;;OAGG;IACH,KAAK,IAAI,OAAO,CAAC,iBAAiB,CAAC,CAAC;IACpC,iEAAiE;IACjE,KAAK,IAAI,OAAO,CAAC,eAAe,GAAG,SAAS,CAAC,CAAC;CAC9C"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/docs/README.md CHANGED
@@ -7,7 +7,7 @@ example per area. These pages are the long one.
7
7
  | --- | --- |
8
8
  | [The sync's lifecycle](guide/sync-lifecycle.md) | you are wiring a collection to an index for the first time, or looking for an option and its default |
9
9
  | [Reindexing](guide/reindex.md) | the index has to be filled, or refilled, from the collection |
10
- | [Following changes](guide/following-changes.md) | a process has to keep the index in step, and stop cleanly — or it stopped and you need to know why |
11
- | [What it leaves out](guide/boundaries.md) | you are about to run two followers, join another collection in, or wonder who applies the index settings |
10
+ | [Following changes](guide/following-changes.md) | a process has to keep the index in step, and stop cleanly; several processes run the same sync and one must follow; or it stopped and you need to know why |
11
+ | [What it leaves out](guide/boundaries.md) | you want to know what the lease on a sync name does not give, are about to join another collection in, or wonder who applies the index settings |
12
12
  | [Troubleshooting](troubleshooting.md) | something threw, and you have the message |
13
13
  | [Roadmap](roadmap.md) | you want to know what is coming, and what will not |
@@ -17,27 +17,35 @@ const articleSearch = createSearchSync({
17
17
  transform: (article) => ({ id: String(article._id), title: article.title }),
18
18
  });
19
19
 
20
- // One follower, in one process: everything below is what that costs.
20
+ // One follower per name: everything below is what that costs.
21
21
  const running = await articleSearch.start();
22
22
  ```
23
23
 
24
- ## Several processes sharing one sync — not yet
24
+ ## One follower per sync name
25
25
 
26
- There is **no lock today**: run one follower per sync name. Two processes
27
- under one name do the same writes twice, and both are wrong the moment one of
28
- them reindexes: a `reindex()` in one while the other follows removes
29
- documents the follower has already indexed and will never send again.
26
+ Several processes may **start** one sync; only one **follows** it at a time.
27
+ `start()` takes a lease on the name, and the others are refused with
28
+ `RUNNING` until it lets go or its lease lapses. What the lease is, and how to
29
+ run a standby, is in
30
+ [Following changes](following-changes.md#one-process-per-name-the-lease).
30
31
 
31
- Inside **one** process the package does refuse it: a second `start()`, or a
32
- `reindex()`, throws `SearchSyncError` with the code `RUNNING`.
32
+ What it does not give:
33
+
34
+ - **Shared work.** A second process does not take half the changes; it
35
+ waits. To spread the load, run one sync per collection, each under its own
36
+ `name`, in as many processes as you like.
37
+ - **Fencing.** A process stalled for longer than `leaseMs` does not know it
38
+ lost the name until its next renewal, up to `leaseMs / 3` after it wakes,
39
+ and a flush already in flight still finishes after it finds out. It may
40
+ send an older version of a document after the process that took over sent
41
+ a newer one; the index then holds the stale one until that document's next
42
+ change or the next reindex. A reindex removes and records nothing once it
43
+ finds its lease lost, but a page it already sent may still land. Keep `leaseMs`
44
+ well above the longest pause a process may take.
33
45
 
34
46
  Give two syncs different `name`s when they are meant to be independent — the
35
47
  default, `'<collection>:<index uid>'`, is shared by any two syncs over the
36
- same pair.
37
-
38
- A lease on a sync name, renewed while a follower runs, is being worked on;
39
- [the roadmap](../roadmap.md) says where it stands. Until it ships, one writer
40
- per name is the rule.
48
+ same pair, and so is its lease.
41
49
 
42
50
  ## Partial updates
43
51
 
@@ -120,9 +128,9 @@ also writes to, lose documents at the first reindex. The sync owns its index.
120
128
  Meilisearch client, and closes neither.
121
129
  - **The permissions.** The MongoDB user needs `find` and `changeStream` on
122
130
  the collection and `find`, `insert`, `update` and `delete` on
123
- `stateCollection`; the Meilisearch key needs `documents.add`,
124
- `documents.get`, `documents.delete` and `tasks.get`, plus `indexes.create`
125
- unless the index already exists.
131
+ `stateCollection`, where the resume point and the lease both live; the
132
+ Meilisearch key needs `documents.add`, `documents.get`, `documents.delete`
133
+ and `tasks.get`, plus `indexes.create` unless the index already exists.
126
134
 
127
135
  ## Next
128
136
 
@@ -32,7 +32,7 @@ the block.
32
32
  | `ready` | `Promise<void>` | Resolves once changes are being heard. `start` has already awaited it |
33
33
  | `closed` | `Promise<CloseReason>` | `'closed'` after `close()`, `'invalidated'` when the collection was dropped or renamed. **Rejects** with a `SearchSyncError` when an error stopped it |
34
34
  | `flush()` | `Promise<void>` | Sends what is waiting, and records the point it covers |
35
- | `close()` | `Promise<void>` | Flushes, then stops |
35
+ | `close()` | `Promise<void>` | Flushes, stops, and lets go of the name's lease; resolves once it is let go |
36
36
 
37
37
  ## Where it starts from
38
38
 
@@ -75,6 +75,116 @@ the stream itself does not move.
75
75
  Without it, a collection nothing writes to for longer than the server's
76
76
  history covers would need a full reindex at the next start.
77
77
 
78
+ ## One process per name: the lease
79
+
80
+ `start()` takes a lease on the sync's name **before anything else**, the
81
+ first reindex included. It renews the lease every third of `leaseMs` from
82
+ then on — during that first reindex too, which can outlast `leaseMs`, and
83
+ while the sync runs; `close()` lets go of it. A second process that starts
84
+ the same name, or reindexes it, is refused:
85
+
86
+ ```ts
87
+ await articleSearch.start(); // in another process, holding the name
88
+ // SearchSyncError: Search sync "articles:articles" is held by
89
+ // worker-1:4127:66f0c2e5a1b2c3d4e5f60718 until 2026-09-22T09:14:07.512Z: wait
90
+ // for it to close, or for its lease to lapse, before you start it.
91
+ ```
92
+
93
+ The code is `RUNNING`, the same code a second `start()` of the same object
94
+ gets in one process (`is already following changes in this process`), which
95
+ is checked first. The holder is `host:pid:<id>`, and the date is when its
96
+ lease ends unless it is renewed. For `reindex()` the message ends
97
+ `before you reindex.`
98
+
99
+ | Option | Type | Default | Effect |
100
+ | --- | --- | --- | --- |
101
+ | `leaseMs` | `number` | `30000` | How long the lease lasts without being renewed. A running sync renews it every `leaseMs / 3`. A whole number above 0 |
102
+
103
+ ```ts
104
+ const articleSearch = createSearchSync({ collection, index, transform, leaseMs: 60_000 });
105
+ ```
106
+
107
+ - **`close()` resolves once the name is free**, so a `reindex()` or a
108
+ `start()` right after it succeeds. A `start()` that fails lets go too.
109
+ - **A process that dies keeps the name** until its lease lapses, at most
110
+ `leaseMs` later. The next `start()` after that takes it over.
111
+ - **A timed renewal that cannot reach MongoDB is tried again** at the next
112
+ beat; the lease lapses only if none reaches it for a whole `leaseMs`. The
113
+ checks a reindex makes before removing and before recording are not: one
114
+ that cannot reach MongoDB stops the reindex with `FAILED` (`failed checking
115
+ its lease`), since what comes next must not run on a lease nobody
116
+ confirmed.
117
+ - **The lease is a document in `stateCollection`**:
118
+ `{ _id: { lease: <name> }, holder, acquiredAt, expiresAt }`, every time
119
+ the server's own clock, so hosts whose clocks disagree still agree on when
120
+ it lapses. No new collection, no new privilege.
121
+
122
+ ### A standby
123
+
124
+ Several processes can run the same code; one follows, the others wait for
125
+ the name. The loop is `follow()`, in the README's
126
+ [One process per sync name](../../README.md#one-process-per-sync-name): it
127
+ retries `start()` on `RUNNING`, and on `LEASE_LOST`, which a `start()` whose
128
+ first reindex lost the name rejects with.
129
+
130
+ ```ts
131
+ const running = await follow(); // resolves once this process holds the name
132
+ ```
133
+
134
+ ### When the lease is lost
135
+
136
+ A process stalled for longer than `leaseMs` — a long garbage collection, a
137
+ blocked event loop, a paused container — may find, at its next renewal, that
138
+ another process has taken the name over. A lease deleted by hand is found the
139
+ same way. The sync then stops rather than run beside the new holder, whose
140
+ lease it leaves alone.
141
+
142
+ **While following**, `closed` rejects with `LEASE_LOST`, and nothing more is
143
+ sent or recorded beyond a flush already in flight:
144
+
145
+ ```ts
146
+ running.closed.catch((error: SearchSyncError) => {
147
+ // error.code === 'LEASE_LOST'
148
+ // error.message: Search sync "articles:articles" lost its lease: another
149
+ // process holds the name now, or the lease was removed (it lapses when not
150
+ // renewed within 30000 ms). It stopped rather than run beside it.
151
+ process.exit(1); // the supervisor restarts it, and `follow()` waits for the name
152
+ });
153
+ ```
154
+
155
+ **While reindexing** — a standalone `reindex()`, or `start()` during its first
156
+ reindex or the one after a lost history — the lease is checked after each page
157
+ sent, and the server is asked before documents are removed and again before the
158
+ resume point is recorded; `start()` asks once more before it opens the
159
+ follower. The call rejects with `LEASE_LOST` **having recorded nothing**, and
160
+ having removed nothing unless the lease went while the removal itself ran. The
161
+ pages already sent stay in the index.
162
+
163
+ ```ts
164
+ import { SearchSyncError } from '@nxgt/mongo-meilisearch';
165
+
166
+ try {
167
+ await articleSearch.reindex();
168
+ } catch (error) {
169
+ if (error instanceof SearchSyncError && error.code === 'LEASE_LOST') {
170
+ // Another process holds the name now; nothing was recorded.
171
+ } else throw error;
172
+ }
173
+ ```
174
+
175
+ It is a lease, not fencing:
176
+
177
+ - **A stalled follower may send stale data.** Until its next renewal — up to
178
+ `leaseMs / 3` after it wakes — it can still send, and a flush already in
179
+ flight finishes after the loss is found. It may send an older version of a
180
+ document after the new holder sent a newer one; the index then holds the
181
+ stale one until that document's next change, or the next reindex.
182
+ - **A reindex's pages may still land.** It removes and records nothing once
183
+ it finds its lease lost, but a page it sent before finding out stays in the
184
+ index.
185
+
186
+ Pick `leaseMs` well above the longest pause a process may take.
187
+
78
188
  ## When the history is gone
79
189
 
80
190
  MongoDB keeps a bounded history of changes, the oplog. A sync stopped for
@@ -83,7 +193,7 @@ longer than it covers cannot resume from its recorded point. By default
83
193
  decision back:
84
194
 
85
195
  ```ts
86
- import { SearchSyncError } from '@nxgt/mongo-meilisearch';
196
+ import { type RunningSearchSync, SearchSyncError } from '@nxgt/mongo-meilisearch';
87
197
 
88
198
  const articleSearch = createSearchSync({
89
199
  collection,
@@ -136,7 +246,8 @@ running.closed.then(
136
246
  | `HISTORY_LOST` | `start` with `onHistoryLost: 'fail'`, and the recorded point is older than the server's history. `cause` is `@nxgt/mongo`'s `DataError`, `serverCode` 286 or 280 |
137
247
  | `ID_MISMATCH` | the transform gave a document whose primary key is not its index id |
138
248
  | `NOT_A_DOCUMENT` | the transform gave back something that is neither a document nor `null` — a string, a number, an array. The message says its shape, never its value |
139
- | `RUNNING` | a second `start()`, or a `reindex()`, while this sync is already following in this process |
249
+ | `RUNNING` | the name is taken: a second `start()`, or a `reindex()`, while this sync is already following in this process — or another process, or another `createSearchSync` of the same name in this one, holds its lease |
250
+ | `LEASE_LOST` | the lease is no longer this sync's: not renewed within `leaseMs` and taken over, or removed. `closed` rejects with it, and nothing more is sent; `reindex()` and `start()` reject with it while reindexing, having recorded nothing, and removed nothing unless the lease went while the removal itself ran |
140
251
  | `FAILED` | anything else: the transform threw, MongoDB or Meilisearch refused. The message says what the sync was doing, and `cause` carries the original error |
141
252
 
142
253
  [Troubleshooting](../troubleshooting.md) has each message with its fix.
@@ -157,8 +268,9 @@ process.on('SIGINT', stop);
157
268
  await running.closed; // rejects if the sync falls over; the supervisor restarts it
158
269
  ```
159
270
 
160
- One process per sync name: there is no lock, and two followers do the same
161
- writes twice. See [What it leaves out](boundaries.md).
271
+ `close()` lets go of the lease before it resolves, so the next worker to
272
+ start takes the name at once instead of waiting `leaseMs`. See
273
+ [What it leaves out](boundaries.md) for what the lease does not give.
162
274
 
163
275
  ## Signatures
164
276
 
@@ -175,7 +287,7 @@ interface RunningSearchSync extends AsyncDisposable {
175
287
  }
176
288
 
177
289
  class SearchSyncError extends Error {
178
- // 'HISTORY_LOST' | 'ID_MISMATCH' | 'NOT_A_DOCUMENT' | 'RUNNING' | 'FAILED'
290
+ // 'HISTORY_LOST' | 'ID_MISMATCH' | 'NOT_A_DOCUMENT' | 'RUNNING' | 'LEASE_LOST' | 'FAILED'
179
291
  readonly code: SearchSyncErrorCode;
180
292
  readonly sync: string;
181
293
  constructor(message: string, options: SearchSyncErrorOptions);