@hot-updater/server 1.0.0-rc.17 → 1.0.0-rc.18
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/adapters/drizzle.cjs +5 -5
- package/dist/adapters/drizzle.mjs +4 -4
- package/dist/adapters/kysely.cjs +3 -3
- package/dist/adapters/kysely.mjs +2 -2
- package/dist/adapters/mongodb.cjs +3 -3
- package/dist/adapters/mongodb.mjs +2 -2
- package/dist/adapters/mongodbAdapter.cjs +19 -1
- package/dist/adapters/mongodbAdapter.mjs +19 -1
- package/dist/adapters/prisma.cjs +5 -5
- package/dist/adapters/prisma.mjs +4 -4
- package/dist/assembly/assemblePlugins.cjs +47 -28
- package/dist/assembly/assemblePlugins.d.cts +3 -7
- package/dist/assembly/assemblePlugins.d.mts +3 -7
- package/dist/assembly/assemblePlugins.mjs +46 -27
- package/dist/assembly/clientPlugins.cjs +86 -0
- package/dist/assembly/clientPlugins.d.cts +16 -0
- package/dist/assembly/clientPlugins.d.mts +16 -0
- package/dist/assembly/clientPlugins.mjs +86 -0
- package/dist/assembly/configError.cjs +7 -0
- package/dist/assembly/configError.d.cts +7 -0
- package/dist/assembly/configError.d.mts +7 -0
- package/dist/assembly/configError.mjs +7 -0
- package/dist/assembly/databasePlugins.cjs +5 -1
- package/dist/assembly/databasePlugins.d.cts +5 -5
- package/dist/assembly/databasePlugins.d.mts +5 -5
- package/dist/assembly/databasePlugins.mjs +5 -1
- package/dist/assembly/pluginCli.cjs +125 -0
- package/dist/assembly/pluginCli.d.cts +59 -0
- package/dist/assembly/pluginCli.d.mts +59 -0
- package/dist/assembly/pluginCli.mjs +121 -0
- package/dist/createHotUpdaterCore.cjs +14 -16
- package/dist/createHotUpdaterCore.d.cts +10 -3
- package/dist/createHotUpdaterCore.d.mts +10 -3
- package/dist/createHotUpdaterCore.mjs +11 -13
- package/dist/database/aggregateBatching.cjs +626 -0
- package/dist/database/aggregateBatching.d.cts +10 -0
- package/dist/database/aggregateBatching.d.mts +10 -0
- package/dist/database/aggregateBatching.mjs +625 -0
- package/dist/{insights/errors.cjs → database/busy.cjs} +2 -15
- package/dist/database/busy.d.cts +12 -0
- package/dist/database/busy.d.mts +12 -0
- package/dist/{insights/errors.mjs → database/busy.mjs} +3 -14
- package/dist/database/database.cjs +22 -3
- package/dist/database/database.d.cts +15 -2
- package/dist/database/database.d.mts +15 -2
- package/dist/database/database.mjs +22 -3
- package/dist/database/definitions.d.cts +10 -0
- package/dist/database/definitions.d.mts +10 -0
- package/dist/database/engine.d.cts +2 -1
- package/dist/database/engine.d.mts +2 -1
- package/dist/database/engineAggregates.cjs +7 -4
- package/dist/database/engineAggregates.mjs +8 -5
- package/dist/database/engineReads.cjs +5 -3
- package/dist/database/engineReads.mjs +6 -4
- package/dist/database/engineTransaction.cjs +2 -6
- package/dist/database/engineTransaction.mjs +2 -6
- package/dist/database/fence.cjs +2 -1
- package/dist/database/fence.mjs +2 -1
- package/dist/database/index.cjs +17 -8
- package/dist/database/index.d.cts +5 -4
- package/dist/database/index.d.mts +5 -4
- package/dist/database/index.mjs +6 -5
- package/dist/database/kv/kvAdapter.cjs +15 -3
- package/dist/database/kv/kvAdapter.d.cts +8 -0
- package/dist/database/kv/kvAdapter.d.mts +8 -0
- package/dist/database/kv/kvAdapter.mjs +16 -4
- package/dist/database/resolveSchema.cjs +4 -1
- package/dist/database/resolveSchema.d.cts +1 -1
- package/dist/database/resolveSchema.d.mts +1 -1
- package/dist/database/resolveSchema.mjs +4 -1
- package/dist/database/retention.cjs +92 -0
- package/dist/database/retention.mjs +90 -0
- package/dist/database/schema.cjs +5 -2
- package/dist/database/schema.d.cts +30 -1
- package/dist/database/schema.d.mts +30 -1
- package/dist/database/schema.mjs +5 -2
- package/dist/database/sql/sqlAdapter.cjs +8 -0
- package/dist/database/sql/sqlAdapter.mjs +9 -1
- package/dist/database/sql/sqlSchema.cjs +56 -0
- package/dist/database/sql/sqlSchema.mjs +55 -1
- package/dist/db/coreDatabase.cjs +85 -0
- package/dist/db/coreDatabase.d.cts +57 -0
- package/dist/db/coreDatabase.d.mts +57 -0
- package/dist/db/coreDatabase.mjs +78 -0
- package/dist/db/index.cjs +11 -1
- package/dist/db/index.d.cts +7 -2
- package/dist/db/index.d.mts +7 -2
- package/dist/db/index.mjs +6 -2
- package/dist/db/types.d.cts +3 -3
- package/dist/db/types.d.mts +3 -3
- package/dist/handler.cjs +2 -14
- package/dist/handler.mjs +1 -13
- package/dist/handlerReleaseCatalogRoutes.cjs +21 -2
- package/dist/handlerReleaseCatalogRoutes.mjs +21 -2
- package/dist/index.cjs +2 -6
- package/dist/index.d.cts +4 -7
- package/dist/index.d.mts +4 -7
- package/dist/index.mjs +2 -4
- package/dist/package.cjs +1 -1
- package/dist/package.mjs +1 -1
- package/dist/{apiKeys.cjs → plugins/api-keys/apiKeys.cjs} +1 -1
- package/dist/{apiKeys.d.cts → plugins/api-keys/apiKeys.d.cts} +2 -2
- package/dist/{apiKeys.d.mts → plugins/api-keys/apiKeys.d.mts} +2 -2
- package/dist/{apiKeys.mjs → plugins/api-keys/apiKeys.mjs} +1 -1
- package/dist/plugins/api-keys/cli.cjs +115 -0
- package/dist/plugins/api-keys/cli.mjs +114 -0
- package/dist/plugins/api-keys/index.cjs +9 -6
- package/dist/plugins/api-keys/index.d.cts +4 -5
- package/dist/plugins/api-keys/index.d.mts +4 -5
- package/dist/plugins/api-keys/index.mjs +9 -7
- package/dist/plugins/api-keys/model.d.cts +20 -2
- package/dist/plugins/api-keys/model.d.mts +20 -2
- package/dist/plugins/definePlugin.cjs +1 -1
- package/dist/plugins/definePlugin.d.cts +130 -3
- package/dist/plugins/definePlugin.d.mts +130 -3
- package/dist/plugins/definePlugin.mjs +1 -1
- package/dist/plugins/index.cjs +25 -4
- package/dist/plugins/index.d.cts +7 -4
- package/dist/plugins/index.d.mts +7 -4
- package/dist/plugins/index.mjs +6 -4
- package/dist/plugins/insights/contract.cjs +193 -0
- package/dist/plugins/insights/contract.mjs +189 -0
- package/dist/{insights → plugins/insights}/domain.d.cts +60 -15
- package/dist/{insights → plugins/insights}/domain.d.mts +60 -15
- package/dist/plugins/insights/errors.cjs +15 -0
- package/dist/plugins/insights/errors.mjs +14 -0
- package/dist/{insights → plugins/insights}/eventInput.cjs +155 -22
- package/dist/{insights → plugins/insights}/eventInput.mjs +155 -22
- package/dist/plugins/insights/eventRow.cjs +49 -0
- package/dist/plugins/insights/eventRow.d.cts +64 -0
- package/dist/plugins/insights/eventRow.d.mts +64 -0
- package/dist/plugins/insights/eventRow.mjs +47 -0
- package/dist/plugins/insights/index.cjs +106 -26
- package/dist/plugins/insights/index.d.cts +130 -27
- package/dist/plugins/insights/index.d.mts +130 -27
- package/dist/plugins/insights/index.mjs +106 -29
- package/dist/plugins/insights/model.cjs +2 -2
- package/dist/plugins/insights/model.d.cts +1 -1
- package/dist/plugins/insights/model.d.mts +1 -1
- package/dist/plugins/insights/model.mjs +1 -1
- package/dist/plugins/insights/modelTypes.d.cts +176 -0
- package/dist/plugins/insights/modelTypes.d.mts +176 -0
- package/dist/plugins/insights/overview.cjs +109 -0
- package/dist/plugins/insights/overview.d.cts +15 -0
- package/dist/plugins/insights/overview.d.mts +15 -0
- package/dist/plugins/insights/overview.mjs +107 -0
- package/dist/{insights → plugins/insights}/provider.cjs +33 -14
- package/dist/{insights → plugins/insights}/provider.d.cts +2 -2
- package/dist/{insights → plugins/insights}/provider.d.mts +2 -2
- package/dist/{insights → plugins/insights}/provider.mjs +30 -11
- package/dist/{insights → plugins/insights}/queryInput.cjs +2 -2
- package/dist/{insights → plugins/insights}/queryInput.mjs +2 -2
- package/dist/plugins/insights/reads.cjs +298 -88
- package/dist/plugins/insights/reads.d.cts +61 -0
- package/dist/plugins/insights/reads.d.mts +61 -0
- package/dist/plugins/insights/reads.mjs +298 -90
- package/dist/plugins/insights/recordEvent.cjs +223 -39
- package/dist/plugins/insights/recordEvent.d.cts +9 -4
- package/dist/plugins/insights/recordEvent.d.mts +9 -4
- package/dist/plugins/insights/recordEvent.mjs +221 -37
- package/dist/{insights → plugins/insights}/routes.cjs +3 -30
- package/dist/{insights → plugins/insights}/routes.mjs +4 -28
- package/dist/plugins/insights/schema.cjs +182 -40
- package/dist/plugins/insights/schema.d.cts +446 -6
- package/dist/plugins/insights/schema.d.mts +446 -6
- package/dist/plugins/insights/schema.mjs +180 -41
- package/dist/plugins/insights/testing/eventRow.d.mts +64 -0
- package/dist/plugins/insights/testing/expectInsightsIndex.mjs +18 -0
- package/dist/plugins/insights/testing/fixtures.d.mts +8 -0
- package/dist/plugins/insights/testing/fixtures.mjs +25 -0
- package/dist/plugins/insights/testing/index.d.mts +24 -0
- package/dist/plugins/insights/testing/index.mjs +25 -0
- package/dist/plugins/insights/testing/insightsModelTests.mjs +714 -0
- package/dist/plugins/insights/testing/modelTypes.d.mts +176 -0
- package/dist/plugins/insights/testing/setupInsightsHttpTestSuite.d.mts +11 -0
- package/dist/plugins/insights/testing/setupInsightsHttpTestSuite.mjs +107 -0
- package/dist/plugins/insights/testing/setupInsightsModelTestSuite.d.mts +11 -0
- package/dist/plugins/insights/testing/setupInsightsModelTestSuite.mjs +31 -0
- package/dist/plugins/insights/testing/types.d.mts +26 -0
- package/dist/{insights → plugins/insights}/types.d.cts +3 -2
- package/dist/{insights → plugins/insights}/types.d.mts +3 -2
- package/package.json +14 -7
- package/src/adapters/drizzle.conformance.integration.spec.ts +2 -2
- package/src/adapters/drizzle.readBudgets.integration.spec.ts +6 -4
- package/src/adapters/drizzle.spec.ts +15 -7
- package/src/adapters/drizzle.ts +6 -6
- package/src/adapters/kysely.conformance.integration.spec.ts +2 -2
- package/src/adapters/kysely.readBudgets.integration.spec.ts +4 -2
- package/src/adapters/kysely.spec.ts +13 -9
- package/src/adapters/kysely.ts +2 -2
- package/src/adapters/mongodb.integration.spec.ts +98 -7
- package/src/adapters/mongodb.ts +2 -2
- package/src/adapters/mongodbAdapter.ts +14 -0
- package/src/adapters/prisma.conformance.integration.spec.ts +2 -2
- package/src/adapters/prisma.integration.spec.ts +16 -8
- package/src/adapters/prisma.readBudgets.integration.spec.ts +5 -2
- package/src/adapters/prisma.spec.ts +17 -9
- package/src/adapters/prisma.ts +6 -6
- package/src/adapters/prismaTestClients.ts +8 -1
- package/src/assembly/assemblePlugins.ts +73 -40
- package/src/assembly/assembly.spec.ts +63 -11
- package/src/assembly/assembly.types.spec.ts +50 -2
- package/src/assembly/clientPlugins.ts +113 -0
- package/src/assembly/configError.ts +4 -0
- package/src/assembly/databasePlugins.ts +11 -5
- package/src/assembly/pluginCli.spec.ts +254 -0
- package/src/assembly/pluginCli.ts +248 -0
- package/src/assembly/{builtInRoutes.spec.ts → pluginRoutes.spec.ts} +10 -37
- package/src/createHotUpdaterCore.ts +21 -17
- package/src/database/aggregateBatching.spec.ts +577 -0
- package/src/database/aggregateBatching.ts +803 -0
- package/src/{insights/errors.spec.ts → database/busy.spec.ts} +2 -5
- package/src/{insights/errors.ts → database/busy.ts} +1 -13
- package/src/database/database.ts +39 -8
- package/src/database/definitions.ts +4 -0
- package/src/database/engineAggregates.spec.ts +4 -7
- package/src/database/engineAggregates.ts +5 -9
- package/src/database/engineReads.ts +7 -5
- package/src/database/engineTransaction.ts +1 -7
- package/src/database/fence.spec.ts +5 -5
- package/src/database/fence.ts +4 -0
- package/src/database/index.ts +13 -7
- package/src/database/kv/kvAdapter.batching.integration.spec.ts +15 -0
- package/src/database/kv/kvAdapter.conformance.integration.spec.ts +2 -0
- package/src/database/kv/kvAdapter.spec.ts +128 -1
- package/src/database/kv/kvAdapter.ts +30 -1
- package/src/database/kv/kvTestStore.ts +28 -1
- package/src/database/resolveSchema.ts +4 -1
- package/src/database/retention.spec.ts +314 -0
- package/src/database/retention.ts +129 -0
- package/src/database/schema.ts +44 -0
- package/src/database/sql/sqlAdapter.integration.spec.ts +4 -7
- package/src/database/sql/sqlAdapter.readBudgets.integration.spec.ts +18 -4
- package/src/database/sql/sqlAdapter.spec.ts +91 -3
- package/src/database/sql/sqlAdapter.ts +20 -0
- package/src/database/sql/sqlSchema.ts +81 -0
- package/src/databaseTestSuite.spec.ts +9 -4
- package/src/db/coreDatabase.ts +153 -0
- package/src/db/engineSql.spec.ts +8 -7
- package/src/db/index.ts +19 -1
- package/src/db/pluginTables.spec.ts +57 -27
- package/src/db/types.ts +3 -3
- package/src/handler-standalone.integration.spec.ts +2 -3
- package/src/handler.ts +1 -32
- package/src/handlerReleaseCatalogRoutes.ts +26 -2
- package/src/index.ts +0 -10
- package/src/{apiKeys.integration.spec.ts → plugins/api-keys/apiKeys.integration.spec.ts} +4 -4
- package/src/{apiKeys.spec.ts → plugins/api-keys/apiKeys.spec.ts} +3 -3
- package/src/{apiKeys.ts → plugins/api-keys/apiKeys.ts} +1 -1
- package/src/plugins/api-keys/apiKeysPlugin.spec.ts +11 -15
- package/src/plugins/api-keys/cli.spec.ts +187 -0
- package/src/plugins/api-keys/cli.ts +140 -0
- package/src/plugins/api-keys/index.ts +48 -41
- package/src/plugins/api-keys/model.ts +21 -2
- package/src/plugins/definePlugin.ts +144 -2
- package/src/plugins/index.ts +20 -2
- package/src/plugins/insights/contract.spec.ts +285 -0
- package/src/plugins/insights/contract.ts +372 -0
- package/src/{insights → plugins/insights}/domain.ts +73 -15
- package/src/plugins/insights/errors.ts +11 -0
- package/src/{insights → plugins/insights}/eventInput.ts +189 -29
- package/src/plugins/insights/eventRow.ts +209 -0
- package/src/plugins/insights/index.ts +181 -45
- package/src/{insights → plugins/insights}/insights.integration.spec.ts +330 -34
- package/src/plugins/insights/insightsRollout.integration.spec.ts +1 -2
- package/src/plugins/insights/model.spec.ts +3 -5
- package/src/plugins/insights/model.ts +2 -3
- package/src/plugins/insights/modelTypes.ts +197 -0
- package/src/plugins/insights/overview.spec.ts +64 -0
- package/src/plugins/insights/overview.ts +214 -0
- package/src/{insights → plugins/insights}/provider.spec.ts +21 -10
- package/src/{insights → plugins/insights}/provider.ts +58 -21
- package/src/{insights → plugins/insights}/queryInput.ts +2 -2
- package/src/plugins/insights/reads.spec.ts +340 -52
- package/src/plugins/insights/reads.ts +453 -123
- package/src/plugins/insights/recordEvent.spec.ts +474 -20
- package/src/plugins/insights/recordEvent.ts +301 -63
- package/src/plugins/insights/retention.spec.ts +115 -0
- package/src/{insights → plugins/insights}/routes.ts +1 -31
- package/src/plugins/insights/schema.ts +356 -176
- package/src/plugins/insights/testing/expectInsightsIndex.spec.ts +42 -0
- package/src/plugins/insights/testing/expectInsightsIndex.ts +20 -0
- package/src/plugins/insights/testing/fixtures.ts +30 -0
- package/src/plugins/insights/testing/index.ts +45 -0
- package/src/plugins/insights/testing/insightsModelTests.ts +860 -0
- package/src/plugins/insights/testing/setupInsightsHttpTestSuite.ts +152 -0
- package/src/plugins/insights/testing/setupInsightsModelTestSuite.ts +36 -0
- package/src/plugins/insights/testing/types.ts +30 -0
- package/src/{insights → plugins/insights}/types.ts +1 -1
- package/src/readBudgets.testFixtures.ts +2 -2
- package/src/releaseCatalogRoutes.integration.spec.ts +7 -0
- package/src/retention.spec.ts +73 -0
- package/src/runtime.spec.ts +11 -12
- package/src/runtime.testFixtures.ts +7 -2
- package/src/testUtilsMutation.integration.spec.ts +2 -1
- package/src/testUtilsPackage.integration.spec.ts +7 -3
- package/dist/db/builtInDatabase.cjs +0 -104
- package/dist/db/builtInDatabase.d.cts +0 -38
- package/dist/db/builtInDatabase.d.mts +0 -38
- package/dist/db/builtInDatabase.mjs +0 -97
- package/dist/plugins/builtIn.cjs +0 -7
- package/dist/plugins/builtIn.mjs +0 -6
- package/src/db/builtInDatabase.ts +0 -149
- package/src/plugins/builtIn.ts +0 -5
package/dist/database/index.mjs
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
|
-
import { DatabaseAmbiguousCommitError, DatabaseConflictError, DatabaseConstraintError, DatabaseTransactionError } from "./errors.mjs";
|
|
2
1
|
import { DatabaseCursorError } from "./cursor.mjs";
|
|
2
|
+
import { DatabaseAmbiguousCommitError, DatabaseConflictError, DatabaseConstraintError, DatabaseTransactionError } from "./errors.mjs";
|
|
3
|
+
import { defineAggregate, defineTable } from "./schema.mjs";
|
|
4
|
+
import { aggregateBatchingModule } from "./aggregateBatching.mjs";
|
|
3
5
|
import { MAX_SHARDS, SHARD_COLUMN, resolveSchema, validateSchema } from "./resolveSchema.mjs";
|
|
4
6
|
import { DatabaseQueryError } from "./engineReads.mjs";
|
|
5
7
|
import { createEngine } from "./engine.mjs";
|
|
6
8
|
import { createDatabaseEngine } from "./database.mjs";
|
|
7
|
-
import { defineAggregate, defineTable } from "./schema.mjs";
|
|
8
9
|
import { ENGINE_SCHEMA_KEY, ENGINE_SCHEMA_VERSION, SETTINGS_TABLE, checkSchemaFence, isMissingSchemaError, withSchemaFence } from "./fence.mjs";
|
|
9
10
|
import { migrateSchema, writeSchemaSettings } from "../db/schemaSettings.mjs";
|
|
10
11
|
import { createTableStatements, isMultiIndex } from "./sql/sqlSchema.mjs";
|
|
11
|
-
import {
|
|
12
|
+
import { coreSchema, coreSettings, coreTarget, createEngineDatabase, migrateCoreSchema, toolingTargetOf } from "../db/coreDatabase.mjs";
|
|
12
13
|
import { WRITE_GUARD_TABLE, classifySqlError, createSqlAdapter } from "./sql/sqlAdapter.mjs";
|
|
13
14
|
import { createKvAdapter, encodeKvKey } from "./kv/kvAdapter.mjs";
|
|
14
|
-
import { DATABASE_MAX_MULTI_VALUES, DATABASE_MAX_QUERY_LIMIT, DATABASE_VERSION_COLUMN, DatabaseAdapterContractError, DatabaseSchemaError, DatabaseValueError, compareTuples, compareUtf8, createMemoryAdapter, indexOrderColumns, normalizeStoredRow, normalizeStoredValue, verifyAdapter } from "@hot-updater/plugin-core/internal";
|
|
15
|
-
export { DATABASE_MAX_MULTI_VALUES, DATABASE_MAX_QUERY_LIMIT, DATABASE_VERSION_COLUMN, DatabaseAdapterContractError, DatabaseAmbiguousCommitError, DatabaseConflictError, DatabaseConstraintError, DatabaseCursorError, DatabaseQueryError, DatabaseSchemaError, DatabaseTransactionError, DatabaseValueError, ENGINE_SCHEMA_KEY, ENGINE_SCHEMA_VERSION, MAX_SHARDS, SETTINGS_TABLE, SHARD_COLUMN, WRITE_GUARD_TABLE,
|
|
15
|
+
import { DATABASE_MAX_MULTI_VALUES, DATABASE_MAX_QUERY_LIMIT, DATABASE_VERSION_COLUMN, DatabaseAdapterContractError, DatabaseSchemaError, DatabaseValueError, compareTuples, compareUtf8, createMemoryAdapter, expiresAt, indexOrderColumns, normalizeStoredRow, normalizeStoredValue, verifyAdapter } from "@hot-updater/plugin-core/internal";
|
|
16
|
+
export { DATABASE_MAX_MULTI_VALUES, DATABASE_MAX_QUERY_LIMIT, DATABASE_VERSION_COLUMN, DatabaseAdapterContractError, DatabaseAmbiguousCommitError, DatabaseConflictError, DatabaseConstraintError, DatabaseCursorError, DatabaseQueryError, DatabaseSchemaError, DatabaseTransactionError, DatabaseValueError, ENGINE_SCHEMA_KEY, ENGINE_SCHEMA_VERSION, MAX_SHARDS, SETTINGS_TABLE, SHARD_COLUMN, WRITE_GUARD_TABLE, aggregateBatchingModule, checkSchemaFence, classifySqlError, compareTuples, compareUtf8, coreSchema, coreSettings, coreTarget, createDatabaseEngine, createEngine, createEngineDatabase, createKvAdapter, createMemoryAdapter, createSqlAdapter, createTableStatements, defineAggregate, defineTable, encodeKvKey, expiresAt, indexOrderColumns, isMissingSchemaError, isMultiIndex, migrateCoreSchema, migrateSchema, normalizeStoredRow, normalizeStoredValue, resolveSchema, toolingTargetOf, validateSchema, verifyAdapter, withSchemaFence, writeSchemaSettings };
|
|
@@ -54,6 +54,11 @@ const createKvAdapter = ({ store, tablePrefix = "" }) => {
|
|
|
54
54
|
});
|
|
55
55
|
const partition = (layout, index, eq) => `${layout.rows}#${index.name}#${encodeKvKey(eq)}`;
|
|
56
56
|
const copyOf = (layout, row) => Object.fromEntries(layout.copied.map((name) => [name, row[name] ?? null]));
|
|
57
|
+
/** The expiry every item of `row` carries, for a table with retention. */
|
|
58
|
+
const stamp = (table, row) => {
|
|
59
|
+
const at = row && (0, _hot_updater_plugin_core_internal.expiresAt)(table, row);
|
|
60
|
+
return at === void 0 ? {} : { expiresAt: at };
|
|
61
|
+
};
|
|
57
62
|
/** Every index and unique item a row has, by address. */
|
|
58
63
|
const itemsOf = (layout, row) => {
|
|
59
64
|
const items = /* @__PURE__ */ new Map();
|
|
@@ -87,7 +92,8 @@ const createKvAdapter = ({ store, tablePrefix = "" }) => {
|
|
|
87
92
|
type: "put",
|
|
88
93
|
key: item.key,
|
|
89
94
|
value: copy,
|
|
90
|
-
condition: taken ? { exists: false } : void 0
|
|
95
|
+
condition: taken ? { exists: false } : void 0,
|
|
96
|
+
...stamp(layout.table, after)
|
|
91
97
|
});
|
|
92
98
|
}
|
|
93
99
|
return ops;
|
|
@@ -114,7 +120,8 @@ const createKvAdapter = ({ store, tablePrefix = "" }) => {
|
|
|
114
120
|
[_hot_updater_plugin_core_internal.DATABASE_VERSION_COLUMN]: 1
|
|
115
121
|
},
|
|
116
122
|
init: op.init,
|
|
117
|
-
condition: condition ?? (op.init ? void 0 : { exists: true })
|
|
123
|
+
condition: condition ?? (op.init ? void 0 : { exists: true }),
|
|
124
|
+
...stamp(op.table, op.init)
|
|
118
125
|
}];
|
|
119
126
|
case "delete": return [{
|
|
120
127
|
type: "delete",
|
|
@@ -132,7 +139,8 @@ const createKvAdapter = ({ store, tablePrefix = "" }) => {
|
|
|
132
139
|
type: "put",
|
|
133
140
|
key,
|
|
134
141
|
value: row,
|
|
135
|
-
condition
|
|
142
|
+
condition,
|
|
143
|
+
...stamp(op.table, row)
|
|
136
144
|
}, ...indexOps(layout, before, row)];
|
|
137
145
|
}
|
|
138
146
|
}
|
|
@@ -264,6 +272,10 @@ const createKvAdapter = ({ store, tablePrefix = "" }) => {
|
|
|
264
272
|
failedOp: items[result.failedOp].origin
|
|
265
273
|
} : result;
|
|
266
274
|
},
|
|
275
|
+
...store.deleteConsumed === void 0 ? {} : { deleteConsumed: (table, rows) => {
|
|
276
|
+
const layout = layoutOf(table);
|
|
277
|
+
return store.deleteConsumed(rows.flatMap((row) => [rowItem(layout, (0, _hot_updater_plugin_core_internal.rowKey)(table, row)), ...[...itemsOf(layout, row).values()].map(({ key }) => key)]));
|
|
278
|
+
} },
|
|
267
279
|
migrations: { apply: async () => store.migrations?.apply() },
|
|
268
280
|
...store.dispose === void 0 ? {} : { dispose: () => store.dispose() }
|
|
269
281
|
};
|
|
@@ -14,6 +14,12 @@ type KvCondition = {
|
|
|
14
14
|
type KvOp = {
|
|
15
15
|
readonly key: KvKey;
|
|
16
16
|
readonly condition?: KvCondition;
|
|
17
|
+
/**
|
|
18
|
+
* When a put's or a created item expires, in epoch milliseconds, which
|
|
19
|
+
* the store maps to its native TTL. Every item of a row of a table with
|
|
20
|
+
* retention carries it, index copies included.
|
|
21
|
+
*/
|
|
22
|
+
readonly expiresAt?: number;
|
|
17
23
|
} & ({
|
|
18
24
|
readonly type: "put";
|
|
19
25
|
readonly value: StoredRow;
|
|
@@ -76,6 +82,8 @@ interface KeyValueStore {
|
|
|
76
82
|
}>;
|
|
77
83
|
/** Applies every op or none; `failedOp` names an op whose condition failed. */
|
|
78
84
|
write(ops: readonly KvOp[]): Promise<WriteResult>;
|
|
85
|
+
/** Deletes items with no condition and not atomically, a batch at a time. */
|
|
86
|
+
deleteConsumed?(keys: readonly KvKey[]): Promise<void>;
|
|
79
87
|
/** Prepares the store, such as its one table, for `db migrate`. */
|
|
80
88
|
readonly migrations?: {
|
|
81
89
|
apply(): Promise<void>;
|
|
@@ -14,6 +14,12 @@ type KvCondition = {
|
|
|
14
14
|
type KvOp = {
|
|
15
15
|
readonly key: KvKey;
|
|
16
16
|
readonly condition?: KvCondition;
|
|
17
|
+
/**
|
|
18
|
+
* When a put's or a created item expires, in epoch milliseconds, which
|
|
19
|
+
* the store maps to its native TTL. Every item of a row of a table with
|
|
20
|
+
* retention carries it, index copies included.
|
|
21
|
+
*/
|
|
22
|
+
readonly expiresAt?: number;
|
|
17
23
|
} & ({
|
|
18
24
|
readonly type: "put";
|
|
19
25
|
readonly value: StoredRow;
|
|
@@ -76,6 +82,8 @@ interface KeyValueStore {
|
|
|
76
82
|
}>;
|
|
77
83
|
/** Applies every op or none; `failedOp` names an op whose condition failed. */
|
|
78
84
|
write(ops: readonly KvOp[]): Promise<WriteResult>;
|
|
85
|
+
/** Deletes items with no condition and not atomically, a batch at a time. */
|
|
86
|
+
deleteConsumed?(keys: readonly KvKey[]): Promise<void>;
|
|
79
87
|
/** Prepares the store, such as its one table, for `db migrate`. */
|
|
80
88
|
readonly migrations?: {
|
|
81
89
|
apply(): Promise<void>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DATABASE_VERSION_COLUMN, DatabaseAdapterContractError, canonicalJson, compareUtf8, findPhysicalIndex, indexEntries, indexOrderColumns, indexOrderTuple, matchesQuery, rowKey } from "@hot-updater/plugin-core/internal";
|
|
1
|
+
import { DATABASE_VERSION_COLUMN, DatabaseAdapterContractError, canonicalJson, compareUtf8, expiresAt, findPhysicalIndex, indexEntries, indexOrderColumns, indexOrderTuple, matchesQuery, rowKey } from "@hot-updater/plugin-core/internal";
|
|
2
2
|
//#region src/database/kv/kvAdapter.ts
|
|
3
3
|
const SEPARATOR = "";
|
|
4
4
|
/** Order-preserving text for key values; each value ends in the lowest code point any encoding uses. */
|
|
@@ -54,6 +54,11 @@ const createKvAdapter = ({ store, tablePrefix = "" }) => {
|
|
|
54
54
|
});
|
|
55
55
|
const partition = (layout, index, eq) => `${layout.rows}#${index.name}#${encodeKvKey(eq)}`;
|
|
56
56
|
const copyOf = (layout, row) => Object.fromEntries(layout.copied.map((name) => [name, row[name] ?? null]));
|
|
57
|
+
/** The expiry every item of `row` carries, for a table with retention. */
|
|
58
|
+
const stamp = (table, row) => {
|
|
59
|
+
const at = row && expiresAt(table, row);
|
|
60
|
+
return at === void 0 ? {} : { expiresAt: at };
|
|
61
|
+
};
|
|
57
62
|
/** Every index and unique item a row has, by address. */
|
|
58
63
|
const itemsOf = (layout, row) => {
|
|
59
64
|
const items = /* @__PURE__ */ new Map();
|
|
@@ -87,7 +92,8 @@ const createKvAdapter = ({ store, tablePrefix = "" }) => {
|
|
|
87
92
|
type: "put",
|
|
88
93
|
key: item.key,
|
|
89
94
|
value: copy,
|
|
90
|
-
condition: taken ? { exists: false } : void 0
|
|
95
|
+
condition: taken ? { exists: false } : void 0,
|
|
96
|
+
...stamp(layout.table, after)
|
|
91
97
|
});
|
|
92
98
|
}
|
|
93
99
|
return ops;
|
|
@@ -114,7 +120,8 @@ const createKvAdapter = ({ store, tablePrefix = "" }) => {
|
|
|
114
120
|
[DATABASE_VERSION_COLUMN]: 1
|
|
115
121
|
},
|
|
116
122
|
init: op.init,
|
|
117
|
-
condition: condition ?? (op.init ? void 0 : { exists: true })
|
|
123
|
+
condition: condition ?? (op.init ? void 0 : { exists: true }),
|
|
124
|
+
...stamp(op.table, op.init)
|
|
118
125
|
}];
|
|
119
126
|
case "delete": return [{
|
|
120
127
|
type: "delete",
|
|
@@ -132,7 +139,8 @@ const createKvAdapter = ({ store, tablePrefix = "" }) => {
|
|
|
132
139
|
type: "put",
|
|
133
140
|
key,
|
|
134
141
|
value: row,
|
|
135
|
-
condition
|
|
142
|
+
condition,
|
|
143
|
+
...stamp(op.table, row)
|
|
136
144
|
}, ...indexOps(layout, before, row)];
|
|
137
145
|
}
|
|
138
146
|
}
|
|
@@ -264,6 +272,10 @@ const createKvAdapter = ({ store, tablePrefix = "" }) => {
|
|
|
264
272
|
failedOp: items[result.failedOp].origin
|
|
265
273
|
} : result;
|
|
266
274
|
},
|
|
275
|
+
...store.deleteConsumed === void 0 ? {} : { deleteConsumed: (table, rows) => {
|
|
276
|
+
const layout = layoutOf(table);
|
|
277
|
+
return store.deleteConsumed(rows.flatMap((row) => [rowItem(layout, rowKey(table, row)), ...[...itemsOf(layout, row).values()].map(({ key }) => key)]));
|
|
278
|
+
} },
|
|
267
279
|
migrations: { apply: async () => store.migrations?.apply() },
|
|
268
280
|
...store.dispose === void 0 ? {} : { dispose: () => store.dispose() }
|
|
269
281
|
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
const require_retention = require("./retention.cjs");
|
|
1
2
|
let _hot_updater_plugin_core_internal = require("@hot-updater/plugin-core/internal");
|
|
2
3
|
//#region src/database/resolveSchema.ts
|
|
3
4
|
const SHARD_COLUMN = "_shard";
|
|
@@ -98,6 +99,7 @@ const problemsOf = (module, model, definition) => {
|
|
|
98
99
|
}
|
|
99
100
|
if (definition.kind === "table") tableProblems(definition, add, table);
|
|
100
101
|
else aggregateProblems(definition, add);
|
|
102
|
+
for (const problem of require_retention.retentionProblems(module.schema, model)) add(problem);
|
|
101
103
|
return problems;
|
|
102
104
|
};
|
|
103
105
|
/** Rejects invalid module schemas at startup, listing every problem. */
|
|
@@ -177,7 +179,8 @@ const resolveSchema = (modules) => {
|
|
|
177
179
|
name,
|
|
178
180
|
columns,
|
|
179
181
|
key: definition.kind === "aggregate" ? [...definition.key, SHARD_COLUMN] : [...definition.key],
|
|
180
|
-
indexes: indexesOf(definition)
|
|
182
|
+
indexes: indexesOf(definition),
|
|
183
|
+
...require_retention.physicalRetention(definition)
|
|
181
184
|
},
|
|
182
185
|
references: references.filter(({ table }) => table === name),
|
|
183
186
|
referencedBy,
|
|
@@ -6,7 +6,7 @@ declare const MAX_SHARDS = 64;
|
|
|
6
6
|
interface SchemaModule {
|
|
7
7
|
readonly id: string;
|
|
8
8
|
readonly schema: SchemaShape;
|
|
9
|
-
/** Prefixes this module's table names;
|
|
9
|
+
/** Prefixes this module's table names; a plugin uses its id unless it sets `namespace: false`. */
|
|
10
10
|
readonly namespace?: string;
|
|
11
11
|
}
|
|
12
12
|
interface ResolvedReference {
|
|
@@ -6,7 +6,7 @@ declare const MAX_SHARDS = 64;
|
|
|
6
6
|
interface SchemaModule {
|
|
7
7
|
readonly id: string;
|
|
8
8
|
readonly schema: SchemaShape;
|
|
9
|
-
/** Prefixes this module's table names;
|
|
9
|
+
/** Prefixes this module's table names; a plugin uses its id unless it sets `namespace: false`. */
|
|
10
10
|
readonly namespace?: string;
|
|
11
11
|
}
|
|
12
12
|
interface ResolvedReference {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { physicalRetention, retentionProblems } from "./retention.mjs";
|
|
1
2
|
import { DATABASE_VERSION_COLUMN, DatabaseSchemaError } from "@hot-updater/plugin-core/internal";
|
|
2
3
|
//#region src/database/resolveSchema.ts
|
|
3
4
|
const SHARD_COLUMN = "_shard";
|
|
@@ -98,6 +99,7 @@ const problemsOf = (module, model, definition) => {
|
|
|
98
99
|
}
|
|
99
100
|
if (definition.kind === "table") tableProblems(definition, add, table);
|
|
100
101
|
else aggregateProblems(definition, add);
|
|
102
|
+
for (const problem of retentionProblems(module.schema, model)) add(problem);
|
|
101
103
|
return problems;
|
|
102
104
|
};
|
|
103
105
|
/** Rejects invalid module schemas at startup, listing every problem. */
|
|
@@ -177,7 +179,8 @@ const resolveSchema = (modules) => {
|
|
|
177
179
|
name,
|
|
178
180
|
columns,
|
|
179
181
|
key: definition.kind === "aggregate" ? [...definition.key, SHARD_COLUMN] : [...definition.key],
|
|
180
|
-
indexes: indexesOf(definition)
|
|
182
|
+
indexes: indexesOf(definition),
|
|
183
|
+
...physicalRetention(definition)
|
|
181
184
|
},
|
|
182
185
|
references: references.filter(({ table }) => table === name),
|
|
183
186
|
referencedBy,
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
//#region src/database/retention.ts
|
|
2
|
+
const MINUTE_MS = 6e4;
|
|
3
|
+
const HOUR_MS = 60 * MINUTE_MS;
|
|
4
|
+
const DAY_MS = 24 * HOUR_MS;
|
|
5
|
+
/** A model's retention as its physical table carries it, which each adapter maps natively. */
|
|
6
|
+
const physicalRetention = ({ retention }) => retention === void 0 ? {} : { retention: {
|
|
7
|
+
column: retention.field,
|
|
8
|
+
ms: retention.days * DAY_MS
|
|
9
|
+
} };
|
|
10
|
+
/** Models in a reference or a rooted index, which an expired row's delete would break. */
|
|
11
|
+
const linkedModels = (schema) => Object.entries(schema).flatMap(([name, { fields, indexes }]) => [...Object.values(fields).map(({ references }) => references), ...Object.values(indexes).map(({ root }) => root)].flatMap((link) => link ? [name, link.model] : []));
|
|
12
|
+
/**
|
|
13
|
+
* What is wrong with a model's retention: it counts from an integer field,
|
|
14
|
+
* over whole days, and expired rows are deleted without cascades, counter
|
|
15
|
+
* moves, or root bumps, so the model is in no reference or rooted index.
|
|
16
|
+
*/
|
|
17
|
+
const retentionProblems = (schema, model) => {
|
|
18
|
+
const definition = schema[model];
|
|
19
|
+
if (definition.retention === void 0) return [];
|
|
20
|
+
const { field, days } = definition.retention;
|
|
21
|
+
const declared = definition.fields[field] ?? (definition.kind === "table" ? definition.derived[field] : void 0);
|
|
22
|
+
return [
|
|
23
|
+
...declared?.type !== "integer" || "multi" in declared && declared.multi === true ? [`retention field "${field}" is not an integer field`] : [],
|
|
24
|
+
...Number.isSafeInteger(days) && days >= 1 ? [] : ["retention days must be a whole number of at least 1"],
|
|
25
|
+
...linkedModels(schema).includes(model) ? ["a model with retention is in no reference or rooted index"] : []
|
|
26
|
+
];
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Rows a pass deletes from each table: one statement or two a table, so a
|
|
30
|
+
* pass stays well inside D1's 50 queries an invocation.
|
|
31
|
+
*/
|
|
32
|
+
const PASS_LIMIT = 500;
|
|
33
|
+
/** The lease row: when the next pass is due, in epoch ms. */
|
|
34
|
+
const LEASE_KEY = "retention.nextPassAt";
|
|
35
|
+
/**
|
|
36
|
+
* The adapter, pruning during its writes with no scheduler: once the lease
|
|
37
|
+
* row in `leaseTable` is due, the write that takes it first runs one bounded
|
|
38
|
+
* pass over the tables with retention, and other writers skip. The next pass is due in an hour, or in a minute while a pass still
|
|
39
|
+
* found a full batch of expired rows in some table. A backend that expires
|
|
40
|
+
* rows natively, or a schema without retention, gets its adapter back.
|
|
41
|
+
*/
|
|
42
|
+
const pruneDuringWrites = (adapter, tables, { leaseTable: table, now }) => {
|
|
43
|
+
const expiring = tables.filter(({ retention }) => retention !== void 0);
|
|
44
|
+
if (adapter.prune === void 0 || expiring.length === 0) return adapter;
|
|
45
|
+
const key = [LEASE_KEY];
|
|
46
|
+
let checkAt = 0;
|
|
47
|
+
const lease = async (row, due) => {
|
|
48
|
+
const value = String(due);
|
|
49
|
+
const guard = { v: row === null ? -1 : Number(row._v) };
|
|
50
|
+
const next = {
|
|
51
|
+
key: LEASE_KEY,
|
|
52
|
+
value,
|
|
53
|
+
_v: guard.v + 1
|
|
54
|
+
};
|
|
55
|
+
const op = row === null ? {
|
|
56
|
+
type: "insert",
|
|
57
|
+
table,
|
|
58
|
+
row: next
|
|
59
|
+
} : {
|
|
60
|
+
type: "patch",
|
|
61
|
+
table,
|
|
62
|
+
key,
|
|
63
|
+
set: { value },
|
|
64
|
+
guard,
|
|
65
|
+
previous: row
|
|
66
|
+
};
|
|
67
|
+
return (await adapter.write([op])).ok ? next : null;
|
|
68
|
+
};
|
|
69
|
+
const pass = async () => {
|
|
70
|
+
const at = now();
|
|
71
|
+
if (at < checkAt) return;
|
|
72
|
+
checkAt = at + MINUTE_MS;
|
|
73
|
+
const [row = null] = await adapter.get(table, [key]);
|
|
74
|
+
checkAt = row === null ? at : Number(row.value);
|
|
75
|
+
if (checkAt > at) return;
|
|
76
|
+
const held = await lease(row, at + HOUR_MS);
|
|
77
|
+
if (held === null) return;
|
|
78
|
+
checkAt = at + HOUR_MS;
|
|
79
|
+
const deleted = [];
|
|
80
|
+
for (const t of expiring) deleted.push(await adapter.prune(t, at - t.retention.ms, PASS_LIMIT));
|
|
81
|
+
if (deleted.includes(PASS_LIMIT) && await lease(held, at + MINUTE_MS) !== null) checkAt = at + MINUTE_MS;
|
|
82
|
+
};
|
|
83
|
+
const warn = (error) => console.warn("[hot-updater] Pruning expired rows failed.", error);
|
|
84
|
+
return Object.assign(Object.create(adapter), { async write(ops) {
|
|
85
|
+
await pass().catch(warn);
|
|
86
|
+
return adapter.write(ops);
|
|
87
|
+
} });
|
|
88
|
+
};
|
|
89
|
+
//#endregion
|
|
90
|
+
exports.physicalRetention = physicalRetention;
|
|
91
|
+
exports.pruneDuringWrites = pruneDuringWrites;
|
|
92
|
+
exports.retentionProblems = retentionProblems;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
//#region src/database/retention.ts
|
|
2
|
+
const MINUTE_MS = 6e4;
|
|
3
|
+
const HOUR_MS = 60 * MINUTE_MS;
|
|
4
|
+
const DAY_MS = 24 * HOUR_MS;
|
|
5
|
+
/** A model's retention as its physical table carries it, which each adapter maps natively. */
|
|
6
|
+
const physicalRetention = ({ retention }) => retention === void 0 ? {} : { retention: {
|
|
7
|
+
column: retention.field,
|
|
8
|
+
ms: retention.days * DAY_MS
|
|
9
|
+
} };
|
|
10
|
+
/** Models in a reference or a rooted index, which an expired row's delete would break. */
|
|
11
|
+
const linkedModels = (schema) => Object.entries(schema).flatMap(([name, { fields, indexes }]) => [...Object.values(fields).map(({ references }) => references), ...Object.values(indexes).map(({ root }) => root)].flatMap((link) => link ? [name, link.model] : []));
|
|
12
|
+
/**
|
|
13
|
+
* What is wrong with a model's retention: it counts from an integer field,
|
|
14
|
+
* over whole days, and expired rows are deleted without cascades, counter
|
|
15
|
+
* moves, or root bumps, so the model is in no reference or rooted index.
|
|
16
|
+
*/
|
|
17
|
+
const retentionProblems = (schema, model) => {
|
|
18
|
+
const definition = schema[model];
|
|
19
|
+
if (definition.retention === void 0) return [];
|
|
20
|
+
const { field, days } = definition.retention;
|
|
21
|
+
const declared = definition.fields[field] ?? (definition.kind === "table" ? definition.derived[field] : void 0);
|
|
22
|
+
return [
|
|
23
|
+
...declared?.type !== "integer" || "multi" in declared && declared.multi === true ? [`retention field "${field}" is not an integer field`] : [],
|
|
24
|
+
...Number.isSafeInteger(days) && days >= 1 ? [] : ["retention days must be a whole number of at least 1"],
|
|
25
|
+
...linkedModels(schema).includes(model) ? ["a model with retention is in no reference or rooted index"] : []
|
|
26
|
+
];
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Rows a pass deletes from each table: one statement or two a table, so a
|
|
30
|
+
* pass stays well inside D1's 50 queries an invocation.
|
|
31
|
+
*/
|
|
32
|
+
const PASS_LIMIT = 500;
|
|
33
|
+
/** The lease row: when the next pass is due, in epoch ms. */
|
|
34
|
+
const LEASE_KEY = "retention.nextPassAt";
|
|
35
|
+
/**
|
|
36
|
+
* The adapter, pruning during its writes with no scheduler: once the lease
|
|
37
|
+
* row in `leaseTable` is due, the write that takes it first runs one bounded
|
|
38
|
+
* pass over the tables with retention, and other writers skip. The next pass is due in an hour, or in a minute while a pass still
|
|
39
|
+
* found a full batch of expired rows in some table. A backend that expires
|
|
40
|
+
* rows natively, or a schema without retention, gets its adapter back.
|
|
41
|
+
*/
|
|
42
|
+
const pruneDuringWrites = (adapter, tables, { leaseTable: table, now }) => {
|
|
43
|
+
const expiring = tables.filter(({ retention }) => retention !== void 0);
|
|
44
|
+
if (adapter.prune === void 0 || expiring.length === 0) return adapter;
|
|
45
|
+
const key = [LEASE_KEY];
|
|
46
|
+
let checkAt = 0;
|
|
47
|
+
const lease = async (row, due) => {
|
|
48
|
+
const value = String(due);
|
|
49
|
+
const guard = { v: row === null ? -1 : Number(row._v) };
|
|
50
|
+
const next = {
|
|
51
|
+
key: LEASE_KEY,
|
|
52
|
+
value,
|
|
53
|
+
_v: guard.v + 1
|
|
54
|
+
};
|
|
55
|
+
const op = row === null ? {
|
|
56
|
+
type: "insert",
|
|
57
|
+
table,
|
|
58
|
+
row: next
|
|
59
|
+
} : {
|
|
60
|
+
type: "patch",
|
|
61
|
+
table,
|
|
62
|
+
key,
|
|
63
|
+
set: { value },
|
|
64
|
+
guard,
|
|
65
|
+
previous: row
|
|
66
|
+
};
|
|
67
|
+
return (await adapter.write([op])).ok ? next : null;
|
|
68
|
+
};
|
|
69
|
+
const pass = async () => {
|
|
70
|
+
const at = now();
|
|
71
|
+
if (at < checkAt) return;
|
|
72
|
+
checkAt = at + MINUTE_MS;
|
|
73
|
+
const [row = null] = await adapter.get(table, [key]);
|
|
74
|
+
checkAt = row === null ? at : Number(row.value);
|
|
75
|
+
if (checkAt > at) return;
|
|
76
|
+
const held = await lease(row, at + HOUR_MS);
|
|
77
|
+
if (held === null) return;
|
|
78
|
+
checkAt = at + HOUR_MS;
|
|
79
|
+
const deleted = [];
|
|
80
|
+
for (const t of expiring) deleted.push(await adapter.prune(t, at - t.retention.ms, PASS_LIMIT));
|
|
81
|
+
if (deleted.includes(PASS_LIMIT) && await lease(held, at + MINUTE_MS) !== null) checkAt = at + MINUTE_MS;
|
|
82
|
+
};
|
|
83
|
+
const warn = (error) => console.warn("[hot-updater] Pruning expired rows failed.", error);
|
|
84
|
+
return Object.assign(Object.create(adapter), { async write(ops) {
|
|
85
|
+
await pass().catch(warn);
|
|
86
|
+
return adapter.write(ops);
|
|
87
|
+
} });
|
|
88
|
+
};
|
|
89
|
+
//#endregion
|
|
90
|
+
export { physicalRetention, pruneDuringWrites, retentionProblems };
|
package/dist/database/schema.cjs
CHANGED
|
@@ -4,7 +4,8 @@ const defineTable = (fields, options) => ({
|
|
|
4
4
|
fields,
|
|
5
5
|
key: options.key,
|
|
6
6
|
derived: options.derived ?? {},
|
|
7
|
-
indexes: options.indexes ?? {}
|
|
7
|
+
indexes: options.indexes ?? {},
|
|
8
|
+
...options.retention === void 0 ? {} : { retention: options.retention }
|
|
8
9
|
});
|
|
9
10
|
const defineAggregate = (fields, options) => ({
|
|
10
11
|
kind: "aggregate",
|
|
@@ -14,7 +15,9 @@ const defineAggregate = (fields, options) => ({
|
|
|
14
15
|
gauges: options.gauges ?? [],
|
|
15
16
|
distinct: options.distinct ?? [],
|
|
16
17
|
shards: options.shards ?? 1,
|
|
17
|
-
indexes: options.indexes ?? {}
|
|
18
|
+
indexes: options.indexes ?? {},
|
|
19
|
+
...options.batched ? { batched: true } : {},
|
|
20
|
+
...options.retention === void 0 ? {} : { retention: options.retention }
|
|
18
21
|
});
|
|
19
22
|
//#endregion
|
|
20
23
|
exports.defineAggregate = defineAggregate;
|
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
import { AggregateShape, FieldDefinition, FieldReference, FieldType, IndexDefinition, ReferenceAction, TableShape } from "./definitions.cjs";
|
|
2
2
|
import { DatabaseJson } from "@hot-updater/plugin-core/internal";
|
|
3
3
|
//#region src/database/schema.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* How long a model keeps its rows: `days` past the epoch milliseconds in
|
|
6
|
+
* `field`, an integer field (a table's derived one too, where null keeps
|
|
7
|
+
* the row). Expired rows are deleted without cascades, so a model with
|
|
8
|
+
* retention is in no reference and no rooted index.
|
|
9
|
+
*/
|
|
10
|
+
interface RetentionDefinition<TField extends string = string> {
|
|
11
|
+
readonly field: TField;
|
|
12
|
+
readonly days: number;
|
|
13
|
+
}
|
|
4
14
|
type FieldValue<TType extends FieldType> = TType extends "string" ? string : TType extends "integer" | "number" ? number : TType extends "boolean" ? boolean : DatabaseJson;
|
|
5
15
|
type Fields = Readonly<Record<string, FieldDefinition>>;
|
|
6
16
|
type RowOf<TFields extends Fields> = { -readonly [K in keyof TFields]: TFields[K] extends FieldDefinition<infer TType> ? TFields[K] extends {
|
|
@@ -18,6 +28,10 @@ type DerivedTypes = Readonly<Record<string, FieldType>>;
|
|
|
18
28
|
/** Derived fields over a table's row, by name. */
|
|
19
29
|
type DerivedFields<TFields extends Fields, TDerived extends DerivedTypes> = { readonly [K in keyof TDerived]: DerivedDefinition<RowOf<TFields>, TDerived[K]>; };
|
|
20
30
|
type Indexes = Readonly<Record<string, IndexDefinition>>;
|
|
31
|
+
/** The integer fields, declared or derived, that retention can count from. */
|
|
32
|
+
type IntegerFields<TFields extends Fields, TDerived extends DerivedTypes> = { [K in keyof TFields]: TFields[K] extends {
|
|
33
|
+
readonly type: "integer";
|
|
34
|
+
} ? K : never; }[keyof TFields] | { [K in keyof TDerived]: TDerived[K] extends "integer" ? K : never; }[keyof TDerived];
|
|
21
35
|
type MissingFields<TIndex, TNames extends string> = TIndex extends {
|
|
22
36
|
readonly eq: readonly (infer TEq)[];
|
|
23
37
|
readonly sort: readonly (infer TSort)[];
|
|
@@ -36,6 +50,8 @@ declare const defineTable: <const TFields extends Fields, const TKey extends rea
|
|
|
36
50
|
readonly key: TKey;
|
|
37
51
|
readonly derived?: DerivedFields<TFields, TDerived>;
|
|
38
52
|
readonly indexes?: TIndexes & CheckIndexes<TIndexes, (keyof TFields | keyof TDerived) & string>;
|
|
53
|
+
/** Rows expire `days` after the epoch milliseconds in `field`. */
|
|
54
|
+
readonly retention?: RetentionDefinition<IntegerFields<TFields, TDerived> & string>;
|
|
39
55
|
}) => TableDefinition<TFields, TDerived, TIndexes, TKey>;
|
|
40
56
|
interface AggregateDefinition<TFields extends Fields = Fields, TMetric extends string = string, TIndexes extends Indexes = Indexes, TSketch extends string = string, TKey extends readonly (keyof TFields & string)[] = readonly (keyof TFields & string)[]> extends AggregateShape {
|
|
41
57
|
readonly kind: "aggregate";
|
|
@@ -45,6 +61,12 @@ interface AggregateDefinition<TFields extends Fields = Fields, TMetric extends s
|
|
|
45
61
|
readonly gauges: readonly TMetric[];
|
|
46
62
|
readonly distinct: readonly TSketch[];
|
|
47
63
|
readonly indexes: TIndexes;
|
|
64
|
+
/**
|
|
65
|
+
* On a database that batches aggregates, changes apply after their
|
|
66
|
+
* transaction commits, in one write per row with other transactions'
|
|
67
|
+
* changes, so reads may lag them. Elsewhere they commit with it.
|
|
68
|
+
*/
|
|
69
|
+
readonly batched?: true;
|
|
48
70
|
}
|
|
49
71
|
declare const defineAggregate: <const TFields extends Fields, const TKey extends readonly (keyof TFields & string)[], const TMetric extends string = never, const TIndexes extends Indexes = {}, const TSketch extends string = never>(fields: TFields, options: {
|
|
50
72
|
readonly key: TKey;
|
|
@@ -53,9 +75,16 @@ declare const defineAggregate: <const TFields extends Fields, const TKey extends
|
|
|
53
75
|
readonly distinct?: readonly TSketch[];
|
|
54
76
|
readonly shards?: number;
|
|
55
77
|
readonly indexes?: TIndexes & CheckIndexes<TIndexes, keyof TFields & string>;
|
|
78
|
+
readonly batched?: true;
|
|
79
|
+
/**
|
|
80
|
+
* Rows expire `days` after the epoch milliseconds in `field`, an
|
|
81
|
+
* identity field such as a bucket's start; a change to a row already
|
|
82
|
+
* past it is dropped, so a late gauge decrement never goes below zero.
|
|
83
|
+
*/
|
|
84
|
+
readonly retention?: RetentionDefinition<IntegerFields<TFields, {}> & string>;
|
|
56
85
|
}) => AggregateDefinition<TFields, TMetric, TIndexes, TSketch, TKey>;
|
|
57
86
|
type ModelDefinition = TableDefinition | AggregateDefinition;
|
|
58
87
|
/** A module's models by name: core, or one plugin. */
|
|
59
88
|
type ModuleSchema = Readonly<Record<string, ModelDefinition>>;
|
|
60
89
|
//#endregion
|
|
61
|
-
export { AggregateDefinition, CheckIndex, DerivedDefinition, DerivedFields, type FieldDefinition, type FieldReference, type FieldType, FieldValue, type IndexDefinition, ModelDefinition, ModuleSchema, type ReferenceAction, RowOf, TableDefinition, defineAggregate, defineTable };
|
|
90
|
+
export { AggregateDefinition, CheckIndex, DerivedDefinition, DerivedFields, type FieldDefinition, type FieldReference, type FieldType, FieldValue, type IndexDefinition, ModelDefinition, ModuleSchema, type ReferenceAction, RetentionDefinition, RowOf, TableDefinition, defineAggregate, defineTable };
|
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
import { AggregateShape, FieldDefinition, FieldReference, FieldType, IndexDefinition, ReferenceAction, TableShape } from "./definitions.mjs";
|
|
2
2
|
import { DatabaseJson } from "@hot-updater/plugin-core/internal";
|
|
3
3
|
//#region src/database/schema.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* How long a model keeps its rows: `days` past the epoch milliseconds in
|
|
6
|
+
* `field`, an integer field (a table's derived one too, where null keeps
|
|
7
|
+
* the row). Expired rows are deleted without cascades, so a model with
|
|
8
|
+
* retention is in no reference and no rooted index.
|
|
9
|
+
*/
|
|
10
|
+
interface RetentionDefinition<TField extends string = string> {
|
|
11
|
+
readonly field: TField;
|
|
12
|
+
readonly days: number;
|
|
13
|
+
}
|
|
4
14
|
type FieldValue<TType extends FieldType> = TType extends "string" ? string : TType extends "integer" | "number" ? number : TType extends "boolean" ? boolean : DatabaseJson;
|
|
5
15
|
type Fields = Readonly<Record<string, FieldDefinition>>;
|
|
6
16
|
type RowOf<TFields extends Fields> = { -readonly [K in keyof TFields]: TFields[K] extends FieldDefinition<infer TType> ? TFields[K] extends {
|
|
@@ -18,6 +28,10 @@ type DerivedTypes = Readonly<Record<string, FieldType>>;
|
|
|
18
28
|
/** Derived fields over a table's row, by name. */
|
|
19
29
|
type DerivedFields<TFields extends Fields, TDerived extends DerivedTypes> = { readonly [K in keyof TDerived]: DerivedDefinition<RowOf<TFields>, TDerived[K]>; };
|
|
20
30
|
type Indexes = Readonly<Record<string, IndexDefinition>>;
|
|
31
|
+
/** The integer fields, declared or derived, that retention can count from. */
|
|
32
|
+
type IntegerFields<TFields extends Fields, TDerived extends DerivedTypes> = { [K in keyof TFields]: TFields[K] extends {
|
|
33
|
+
readonly type: "integer";
|
|
34
|
+
} ? K : never; }[keyof TFields] | { [K in keyof TDerived]: TDerived[K] extends "integer" ? K : never; }[keyof TDerived];
|
|
21
35
|
type MissingFields<TIndex, TNames extends string> = TIndex extends {
|
|
22
36
|
readonly eq: readonly (infer TEq)[];
|
|
23
37
|
readonly sort: readonly (infer TSort)[];
|
|
@@ -36,6 +50,8 @@ declare const defineTable: <const TFields extends Fields, const TKey extends rea
|
|
|
36
50
|
readonly key: TKey;
|
|
37
51
|
readonly derived?: DerivedFields<TFields, TDerived>;
|
|
38
52
|
readonly indexes?: TIndexes & CheckIndexes<TIndexes, (keyof TFields | keyof TDerived) & string>;
|
|
53
|
+
/** Rows expire `days` after the epoch milliseconds in `field`. */
|
|
54
|
+
readonly retention?: RetentionDefinition<IntegerFields<TFields, TDerived> & string>;
|
|
39
55
|
}) => TableDefinition<TFields, TDerived, TIndexes, TKey>;
|
|
40
56
|
interface AggregateDefinition<TFields extends Fields = Fields, TMetric extends string = string, TIndexes extends Indexes = Indexes, TSketch extends string = string, TKey extends readonly (keyof TFields & string)[] = readonly (keyof TFields & string)[]> extends AggregateShape {
|
|
41
57
|
readonly kind: "aggregate";
|
|
@@ -45,6 +61,12 @@ interface AggregateDefinition<TFields extends Fields = Fields, TMetric extends s
|
|
|
45
61
|
readonly gauges: readonly TMetric[];
|
|
46
62
|
readonly distinct: readonly TSketch[];
|
|
47
63
|
readonly indexes: TIndexes;
|
|
64
|
+
/**
|
|
65
|
+
* On a database that batches aggregates, changes apply after their
|
|
66
|
+
* transaction commits, in one write per row with other transactions'
|
|
67
|
+
* changes, so reads may lag them. Elsewhere they commit with it.
|
|
68
|
+
*/
|
|
69
|
+
readonly batched?: true;
|
|
48
70
|
}
|
|
49
71
|
declare const defineAggregate: <const TFields extends Fields, const TKey extends readonly (keyof TFields & string)[], const TMetric extends string = never, const TIndexes extends Indexes = {}, const TSketch extends string = never>(fields: TFields, options: {
|
|
50
72
|
readonly key: TKey;
|
|
@@ -53,9 +75,16 @@ declare const defineAggregate: <const TFields extends Fields, const TKey extends
|
|
|
53
75
|
readonly distinct?: readonly TSketch[];
|
|
54
76
|
readonly shards?: number;
|
|
55
77
|
readonly indexes?: TIndexes & CheckIndexes<TIndexes, keyof TFields & string>;
|
|
78
|
+
readonly batched?: true;
|
|
79
|
+
/**
|
|
80
|
+
* Rows expire `days` after the epoch milliseconds in `field`, an
|
|
81
|
+
* identity field such as a bucket's start; a change to a row already
|
|
82
|
+
* past it is dropped, so a late gauge decrement never goes below zero.
|
|
83
|
+
*/
|
|
84
|
+
readonly retention?: RetentionDefinition<IntegerFields<TFields, {}> & string>;
|
|
56
85
|
}) => AggregateDefinition<TFields, TMetric, TIndexes, TSketch, TKey>;
|
|
57
86
|
type ModelDefinition = TableDefinition | AggregateDefinition;
|
|
58
87
|
/** A module's models by name: core, or one plugin. */
|
|
59
88
|
type ModuleSchema = Readonly<Record<string, ModelDefinition>>;
|
|
60
89
|
//#endregion
|
|
61
|
-
export { AggregateDefinition, CheckIndex, DerivedDefinition, DerivedFields, type FieldDefinition, type FieldReference, type FieldType, FieldValue, type IndexDefinition, ModelDefinition, ModuleSchema, type ReferenceAction, RowOf, TableDefinition, defineAggregate, defineTable };
|
|
90
|
+
export { AggregateDefinition, CheckIndex, DerivedDefinition, DerivedFields, type FieldDefinition, type FieldReference, type FieldType, FieldValue, type IndexDefinition, ModelDefinition, ModuleSchema, type ReferenceAction, RetentionDefinition, RowOf, TableDefinition, defineAggregate, defineTable };
|
package/dist/database/schema.mjs
CHANGED
|
@@ -4,7 +4,8 @@ const defineTable = (fields, options) => ({
|
|
|
4
4
|
fields,
|
|
5
5
|
key: options.key,
|
|
6
6
|
derived: options.derived ?? {},
|
|
7
|
-
indexes: options.indexes ?? {}
|
|
7
|
+
indexes: options.indexes ?? {},
|
|
8
|
+
...options.retention === void 0 ? {} : { retention: options.retention }
|
|
8
9
|
});
|
|
9
10
|
const defineAggregate = (fields, options) => ({
|
|
10
11
|
kind: "aggregate",
|
|
@@ -14,7 +15,9 @@ const defineAggregate = (fields, options) => ({
|
|
|
14
15
|
gauges: options.gauges ?? [],
|
|
15
16
|
distinct: options.distinct ?? [],
|
|
16
17
|
shards: options.shards ?? 1,
|
|
17
|
-
indexes: options.indexes ?? {}
|
|
18
|
+
indexes: options.indexes ?? {},
|
|
19
|
+
...options.batched ? { batched: true } : {},
|
|
20
|
+
...options.retention === void 0 ? {} : { retention: options.retention }
|
|
18
21
|
});
|
|
19
22
|
//#endregion
|
|
20
23
|
export { defineAggregate, defineTable };
|
|
@@ -265,6 +265,14 @@ const createSqlAdapter = ({ executor, tablePrefix = "", maxOps = 1e3, maxParams
|
|
|
265
265
|
throw error;
|
|
266
266
|
}
|
|
267
267
|
},
|
|
268
|
+
async prune(table, before, limit) {
|
|
269
|
+
const statements = require_sqlSchema.pruneStatements(dialect, table, tablePrefix, limit).map((render) => sql((bind) => render(() => bind(before))));
|
|
270
|
+
return (executor.batch ? await executor.batch(statements) : await executor.transaction(async (connection) => {
|
|
271
|
+
const done = [];
|
|
272
|
+
for (const statement of statements) done.push(await connection.execute(statement));
|
|
273
|
+
return done;
|
|
274
|
+
})).at(-1)?.changes ?? 0;
|
|
275
|
+
},
|
|
268
276
|
migrations: { async apply(tables) {
|
|
269
277
|
const created = executor.batch && !tables.includes(WRITE_GUARD_TABLE) ? [...tables, WRITE_GUARD_TABLE] : tables;
|
|
270
278
|
const statements = require_sqlSchema.createTableStatements(dialect, created, tablePrefix);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createTableStatements, isMultiIndex, quoteSql } from "./sqlSchema.mjs";
|
|
1
|
+
import { createTableStatements, isMultiIndex, pruneStatements, quoteSql } from "./sqlSchema.mjs";
|
|
2
2
|
import { DATABASE_VERSION_COLUMN, findPhysicalColumn, findPhysicalIndex, indexEntries, indexOrderColumns, indexOrderTuple, normalizeStoredRow, rowKey } from "@hot-updater/plugin-core/internal";
|
|
3
3
|
//#region src/database/sql/sqlAdapter.ts
|
|
4
4
|
/** Where a batch write records its first failed op, before any change applies. */
|
|
@@ -265,6 +265,14 @@ const createSqlAdapter = ({ executor, tablePrefix = "", maxOps = 1e3, maxParams
|
|
|
265
265
|
throw error;
|
|
266
266
|
}
|
|
267
267
|
},
|
|
268
|
+
async prune(table, before, limit) {
|
|
269
|
+
const statements = pruneStatements(dialect, table, tablePrefix, limit).map((render) => sql((bind) => render(() => bind(before))));
|
|
270
|
+
return (executor.batch ? await executor.batch(statements) : await executor.transaction(async (connection) => {
|
|
271
|
+
const done = [];
|
|
272
|
+
for (const statement of statements) done.push(await connection.execute(statement));
|
|
273
|
+
return done;
|
|
274
|
+
})).at(-1)?.changes ?? 0;
|
|
275
|
+
},
|
|
268
276
|
migrations: { async apply(tables) {
|
|
269
277
|
const created = executor.batch && !tables.includes(WRITE_GUARD_TABLE) ? [...tables, WRITE_GUARD_TABLE] : tables;
|
|
270
278
|
const statements = createTableStatements(dialect, created, tablePrefix);
|