@kubun/hub 0.17.2 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -11,6 +11,12 @@ const DEFAULT_RETENTION_INTERVAL_SECONDS = 3600;
11
11
  const DEFAULT_SUBSCRIPTION_RETENTION_SECONDS = 90 * 24 * 60 * 60;
12
12
  const DEFAULT_MAX_RETENTION_SECONDS = 30 * 24 * 60 * 60;
13
13
  const DEFAULT_MAX_DEPTH = 1000;
14
+ // Raised from `@enkaku/http-serve`'s own 1 MiB: a relay exists to carry sync
15
+ // frames, and a full-scope merkle reconciliation of a modest graph (~1.5 MiB
16
+ // observed) already exceeds 1 MiB. 8 MiB clears realistic reconciliations with
17
+ // headroom while still bounding a single request.
18
+ const DEFAULT_MAX_REQUEST_BODY_SIZE = 8 * 1024 * 1024;
19
+ const DEFAULT_MAX_SESSION_BUFFER_BYTES = 8 * 1024 * 1024;
14
20
  function resolveAccessRules(auth) {
15
21
  if (auth == null) return {};
16
22
  switch(auth.mode){
@@ -86,7 +92,9 @@ export async function createRelayInternal(options) {
86
92
  }
87
93
  const allowedOrigin = resolveAllowedOrigin(options.allowedOrigin);
88
94
  const transport = new ServerTransport({
89
- allowedOrigin
95
+ allowedOrigin,
96
+ maxRequestBodySize: options.maxRequestBodySize ?? DEFAULT_MAX_REQUEST_BODY_SIZE,
97
+ maxSessionBufferBytes: options.maxSessionBufferBytes ?? DEFAULT_MAX_SESSION_BUFFER_BYTES
90
98
  });
91
99
  const accessRules = resolveAccessRules(options.auth);
92
100
  // `@kumiai/hub-server` assembles the handlers, registry, and Enkaku server for
@@ -229,6 +237,8 @@ export async function createRelay(options) {
229
237
  authorize: options.authorize,
230
238
  allowedOrigin: options.allowedOrigin,
231
239
  rateLimits: options.rateLimits,
240
+ maxRequestBodySize: options.maxRequestBodySize,
241
+ maxSessionBufferBytes: options.maxSessionBufferBytes,
232
242
  retention: options.retention,
233
243
  defaultRetention: options.defaultRetention,
234
244
  maxRetention: options.maxRetention,
@@ -1,4 +1,4 @@
1
- import type { Adapter } from '@kubun/db-adapter';
1
+ import { type Adapter } from '@kubun/db-adapter';
2
2
  import { type HubStore } from '@kumiai/hub-protocol';
3
3
  import type { ColumnType, Kysely } from 'kysely';
4
4
  export type HubDatabase = {
@@ -33,6 +33,7 @@ export type HubDatabase = {
33
33
  kubun_hub_topics: {
34
34
  topic_id: string;
35
35
  head: string;
36
+ removed_through: ColumnType<string | null, string | null | undefined, string | null>;
36
37
  };
37
38
  kubun_hub_sequence: {
38
39
  id: number;
@@ -48,6 +49,7 @@ export type HubDatabase = {
48
49
  export type CreateHubStoreParams = {
49
50
  kysely: Kysely<HubDatabase> | Promise<Kysely<HubDatabase>>;
50
51
  adapter: Adapter;
52
+ now?: () => number;
51
53
  defaultRetention?: number;
52
54
  maxRetention?: number;
53
55
  maxDepth?: number;
package/lib/hub-store.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { AbstractPostgresAdapter } from '@kubun/db-adapter';
1
2
  import { HeadMismatchError, KeyPackageQuotaExceededError, NotSubscribedError, RetentionExceededError, SubscriptionQuotaExceededError } from '@kumiai/hub-protocol';
2
3
  import { EventEmitter } from '@sozai/event';
3
4
  // Normalize a DB binary column (a Buffer under better-sqlite3 / node-postgres)
@@ -18,6 +19,7 @@ function toBytes(value) {
18
19
  const DEFAULT_MAX_SUBSCRIPTIONS_PER_DID = 1000;
19
20
  export function createHubStore(params) {
20
21
  const { adapter } = params;
22
+ const now = params.now ?? Date.now;
21
23
  const defaultRetention = params.defaultRetention ?? 0;
22
24
  const maxRetention = params.maxRetention;
23
25
  const maxDepth = params.maxDepth;
@@ -39,11 +41,24 @@ export function createHubStore(params) {
39
41
  * variables" past its limit (32766 here, 999 on older builds), so an unchunked
40
42
  * sweep fails precisely when the backlog is large. Deliveries first: a delivery
41
43
  * references a frame and cannot be pushed once its referent is gone.
42
- */ async function deleteFrames(executor, sequenceIDs) {
44
+ */ async function deleteFrames(trx, sequenceIDs) {
43
45
  for(let index = 0; index < sequenceIDs.length; index += DELETE_CHUNK_SIZE){
44
46
  const chunk = sequenceIDs.slice(index, index + DELETE_CHUNK_SIZE);
45
- await executor.deleteFrom('kubun_hub_deliveries').where('sequence_id', 'in', chunk).execute();
46
- await executor.deleteFrom('kubun_hub_messages').where('sequence_id', 'in', chunk).execute();
47
+ const removedPrefixes = await trx.selectFrom('kubun_hub_messages').select((eb)=>[
48
+ 'topic_id',
49
+ eb.fn.max('sequence_id').as('removed_through')
50
+ ]).where('sequence_id', 'in', chunk).where('retain', '=', 'log').groupBy('topic_id').orderBy('topic_id').execute();
51
+ // Take the topic lock before deleting frames, as publication does during depth eviction.
52
+ for (const { topic_id, removed_through } of removedPrefixes){
53
+ await trx.updateTable('kubun_hub_topics').set({
54
+ removed_through
55
+ }).where('topic_id', '=', topic_id).where((eb)=>eb.or([
56
+ eb('removed_through', 'is', null),
57
+ eb('removed_through', '<', removed_through)
58
+ ])).execute();
59
+ }
60
+ await trx.deleteFrom('kubun_hub_deliveries').where('sequence_id', 'in', chunk).execute();
61
+ await trx.deleteFrom('kubun_hub_messages').where('sequence_id', 'in', chunk).execute();
47
62
  }
48
63
  }
49
64
  // Mint the next sequenceID inside the accepting transaction, from a persistent
@@ -115,7 +130,7 @@ export function createHubStore(params) {
115
130
  sender_did: senderDID,
116
131
  topic_id: topicID,
117
132
  payload: adapter.encodeBinary(publishParams.payload),
118
- stored_at: Date.now(),
133
+ stored_at: now(),
119
134
  retain
120
135
  }).execute();
121
136
  // Push is untouched by the class: a log frame is delivered to every current
@@ -172,7 +187,7 @@ export function createHubStore(params) {
172
187
  topic_id: topicID,
173
188
  publish_id: publishParams.publishID,
174
189
  sequence_id: sequenceID,
175
- recorded_at: Date.now()
190
+ recorded_at: now()
176
191
  }).execute();
177
192
  }
178
193
  return {
@@ -236,48 +251,52 @@ export function createHubStore(params) {
236
251
  async function fetchTopic(fetchParams) {
237
252
  const kysely = await db();
238
253
  const { subscriberDID, topicID } = fetchParams;
239
- // Authorization: the caller must be a current subscriber of the topic.
240
- const subscription = await kysely.selectFrom('kubun_hub_subscriptions').select('subscriber_did').where('topic_id', '=', topicID).where('subscriber_did', '=', subscriberDID).executeTakeFirst();
241
- if (subscription == null) {
242
- throw new NotSubscribedError(`${subscriberDID} is not subscribed to ${topicID}`);
243
- }
244
- // Head is stored state — it outlives every frame.
245
- const topicRow = await kysely.selectFrom('kubun_hub_topics').select('head').where('topic_id', '=', topicID).executeTakeFirst();
246
- const head = topicRow?.head ?? null;
247
- // Oldest is the oldest RETAINED log frame, or null if the log is empty.
248
- const oldestRow = await kysely.selectFrom('kubun_hub_messages').select((eb)=>eb.fn.min('sequence_id').as('oldest')).where('topic_id', '=', topicID).where('retain', '=', 'log').executeTakeFirst();
249
- const oldest = oldestRow?.oldest ?? null;
250
- // The log is the topic's log-class frames and NOTHING else — a mailbox frame
251
- // never appears here. `limit` counts log frames (applied after the class
252
- // filter), so a page of mailbox frames cannot hand a draining reader an empty
253
- // page while log frames still wait.
254
- let query = kysely.selectFrom('kubun_hub_messages').select([
255
- 'sequence_id',
256
- 'sender_did',
257
- 'topic_id',
258
- 'payload'
259
- ]).where('topic_id', '=', topicID).where('retain', '=', 'log').orderBy('sequence_id', 'asc');
260
- if (fetchParams.after != null) {
261
- query = query.where('sequence_id', '>', fetchParams.after);
262
- }
263
- if (fetchParams.limit != null) {
264
- query = query.limit(fetchParams.limit);
265
- }
266
- const rows = await query.execute();
267
- // Every row here is log-class by the filter above, so each carries its position
268
- // unconditionally — the same value a push of that frame reports.
269
- const messages = rows.map((row)=>({
270
- sequenceID: row.sequence_id,
271
- senderDID: row.sender_did,
272
- topicID: row.topic_id,
273
- payload: toBytes(row.payload),
274
- logPosition: row.sequence_id
275
- }));
276
- return {
277
- messages,
278
- head,
279
- oldest
280
- };
254
+ // READ COMMITTED takes a new snapshot per statement and can conceal a concurrent trim.
255
+ return await kysely.transaction().setIsolationLevel('repeatable read').setAccessMode('read only').execute(async (trx)=>{
256
+ // Authorization: the caller must be a current subscriber of the topic.
257
+ const subscription = await trx.selectFrom('kubun_hub_subscriptions').select('subscriber_did').where('topic_id', '=', topicID).where('subscriber_did', '=', subscriberDID).executeTakeFirst();
258
+ if (subscription == null) {
259
+ throw new NotSubscribedError(`${subscriberDID} is not subscribed to ${topicID}`);
260
+ }
261
+ // Head and the removal watermark outlive every frame.
262
+ const topicRow = await trx.selectFrom('kubun_hub_topics').select([
263
+ 'head',
264
+ 'removed_through'
265
+ ]).where('topic_id', '=', topicID).executeTakeFirst();
266
+ const head = topicRow?.head ?? null;
267
+ const removedThrough = topicRow?.removed_through ?? null;
268
+ const gap = removedThrough != null && (fetchParams.after == null || removedThrough > fetchParams.after);
269
+ // Oldest is the oldest retained log frame, or null if the log is empty.
270
+ const oldestRow = await trx.selectFrom('kubun_hub_messages').select((eb)=>eb.fn.min('sequence_id').as('oldest')).where('topic_id', '=', topicID).where('retain', '=', 'log').executeTakeFirst();
271
+ const oldest = oldestRow?.oldest ?? null;
272
+ // Mailbox frames cannot consume a page's log limit.
273
+ let query = trx.selectFrom('kubun_hub_messages').select([
274
+ 'sequence_id',
275
+ 'sender_did',
276
+ 'topic_id',
277
+ 'payload'
278
+ ]).where('topic_id', '=', topicID).where('retain', '=', 'log').orderBy('sequence_id', 'asc');
279
+ if (fetchParams.after != null) {
280
+ query = query.where('sequence_id', '>', fetchParams.after);
281
+ }
282
+ if (fetchParams.limit != null) {
283
+ query = query.limit(fetchParams.limit);
284
+ }
285
+ const rows = await query.execute();
286
+ const messages = rows.map((row)=>({
287
+ sequenceID: row.sequence_id,
288
+ senderDID: row.sender_did,
289
+ topicID: row.topic_id,
290
+ payload: toBytes(row.payload),
291
+ logPosition: row.sequence_id
292
+ }));
293
+ return {
294
+ messages,
295
+ head,
296
+ oldest,
297
+ gap
298
+ };
299
+ });
281
300
  }
282
301
  async function ack(ackParams) {
283
302
  if (ackParams.sequenceIDs.length === 0) return;
@@ -296,23 +315,26 @@ export function createHubStore(params) {
296
315
  async function trim(trimParams) {
297
316
  const kysely = await db();
298
317
  // Remove LOG frames strictly below the bound, and only those. A mailbox frame
299
- // on the same topic is delivery-derived and untouched. The head (topics table)
300
- // and the publishID records are never touched.
301
- const logRows = await kysely.selectFrom('kubun_hub_messages').select('sequence_id').where('topic_id', '=', trimParams.topicID).where('retain', '=', 'log').where('sequence_id', '<', trimParams.before).execute();
302
- const sequenceIDs = logRows.map((r)=>r.sequence_id);
303
- if (sequenceIDs.length === 0) return;
304
- await deleteFrames(kysely, sequenceIDs);
318
+ // on the same topic is delivery-derived and untouched. The stored head and
319
+ // publishID records are never touched.
320
+ await kysely.transaction().execute(async (trx)=>{
321
+ // Match purge's topic-before-frame lock order across every deletion chunk.
322
+ const topicQuery = trx.selectFrom('kubun_hub_topics').select('head').where('topic_id', '=', trimParams.topicID);
323
+ await (adapter instanceof AbstractPostgresAdapter ? topicQuery.forUpdate() : topicQuery).executeTakeFirst();
324
+ const logRows = await trx.selectFrom('kubun_hub_messages').select('sequence_id').where('topic_id', '=', trimParams.topicID).where('retain', '=', 'log').where('sequence_id', '<', trimParams.before).orderBy('sequence_id', 'asc').execute();
325
+ await deleteFrames(trx, logRows.map((row)=>row.sequence_id));
326
+ });
305
327
  }
306
328
  async function purge(purgeParams) {
307
329
  const kysely = await db();
308
- const now = Date.now();
330
+ const purgeTime = now();
309
331
  const olderThan = purgeParams.olderThan;
310
332
  // Idempotency records outlive the frames they dedup, on their own clock: the
311
333
  // contract is that no message deleter reaches one for a frame it still
312
334
  // guards, NOT that they are kept forever. Every commit publish writes one, so
313
335
  // without this the table is the fastest-growing on a long-lived hub. The
314
336
  // window far exceeds any plausible replay (see PUBLISH_ID_RETENTION_SECONDS).
315
- await kysely.deleteFrom('kubun_hub_publish_ids').where('recorded_at', '<=', now - PUBLISH_ID_RETENTION_SECONDS * 1000).execute();
337
+ await kysely.deleteFrom('kubun_hub_publish_ids').where('recorded_at', '<=', purgeTime - PUBLISH_ID_RETENTION_SECONDS * 1000).execute();
316
338
  // Per topic, the effective age bound is the longest retention any CURRENT
317
339
  // subscriber asked for, floored at the hub default (and at the sweep's
318
340
  // olderThan). Applies to both classes. Never touches the head or a publishID
@@ -330,25 +352,66 @@ export function createHubStore(params) {
330
352
  Number(row.maxRetention ?? 0)
331
353
  ]));
332
354
  const removed = [];
333
- for (const { topic_id } of topicRows){
334
- const subscriberRetention = retentionByTopic.get(topic_id) ?? 0;
335
- const effective = Math.max(olderThan, subscriberRetention, defaultRetention);
336
- const threshold = now - effective * 1000;
337
- const frames = await kysely.selectFrom('kubun_hub_messages').select('sequence_id').where('topic_id', '=', topic_id).where('stored_at', '<=', threshold).execute();
338
- for (const frame of frames){
339
- removed.push(frame.sequence_id);
355
+ try {
356
+ for (const { topic_id } of topicRows){
357
+ const subscriberRetention = retentionByTopic.get(topic_id) ?? 0;
358
+ const effective = Math.max(olderThan, subscriberRetention, defaultRetention);
359
+ const threshold = purgeTime - effective * 1000;
360
+ // One topic per transaction releases SQLite's connection between topics.
361
+ const topicRemoved = await kysely.transaction().setIsolationLevel('read committed').execute(async (trx)=>{
362
+ // Publication takes this row before committing its head and depth eviction.
363
+ // SQLite's single connection already serializes the transaction.
364
+ const topicQuery = trx.selectFrom('kubun_hub_topics').select('head').where('topic_id', '=', topic_id);
365
+ const topicRow = await (adapter instanceof AbstractPostgresAdapter ? topicQuery.forUpdate() : topicQuery).executeTakeFirst();
366
+ // Clock regression must not punch a hole behind the first retained frame.
367
+ const retainedLog = await trx.selectFrom('kubun_hub_messages').select((eb)=>eb.fn.min('sequence_id').as('cut')).where('topic_id', '=', topic_id).where('retain', '=', 'log').where('stored_at', '>', threshold).executeTakeFirstOrThrow();
368
+ const deletionCandidates = trx.selectFrom('kubun_hub_messages').select('sequence_id').where('topic_id', '=', topic_id).where((eb)=>{
369
+ const mailbox = eb.and([
370
+ eb('retain', '=', 'mailbox'),
371
+ eb('stored_at', '<=', threshold)
372
+ ]);
373
+ // A mailbox-only topic has no lockable row. Its first log must survive,
374
+ // and a null cut cannot admit frames newer than the captured head.
375
+ if (topicRow == null) return mailbox;
376
+ const log = [
377
+ eb('retain', '=', 'log'),
378
+ eb('sequence_id', '<=', topicRow.head)
379
+ ];
380
+ if (retainedLog.cut != null) {
381
+ log.push(eb('sequence_id', '<', retainedLog.cut));
382
+ }
383
+ return eb.or([
384
+ eb.and(log),
385
+ mailbox
386
+ ]);
387
+ }).orderBy('sequence_id', 'asc').limit(DELETE_CHUNK_SIZE);
388
+ const sequenceIDs = [];
389
+ while(true){
390
+ const frames = await deletionCandidates.execute();
391
+ if (frames.length === 0) break;
392
+ const chunk = frames.map((frame)=>frame.sequence_id);
393
+ await deleteFrames(trx, chunk);
394
+ for (const sequenceID of chunk){
395
+ sequenceIDs.push(sequenceID);
396
+ }
397
+ if (frames.length < DELETE_CHUNK_SIZE) break;
398
+ }
399
+ return sequenceIDs;
400
+ });
401
+ for (const sequenceID of topicRemoved){
402
+ removed.push(sequenceID);
403
+ }
340
404
  }
405
+ } finally{
406
+ if (removed.length > 0) await events.emit('purge', {
407
+ sequenceIDs: removed
408
+ });
341
409
  }
342
- if (removed.length === 0) return [];
343
- await deleteFrames(kysely, removed);
344
- await events.emit('purge', {
345
- sequenceIDs: removed
346
- });
347
410
  return removed;
348
411
  }
349
412
  // `not_after` is seconds; every other timestamp here is milliseconds.
350
413
  function nowSeconds() {
351
- return Math.floor(Date.now() / 1000);
414
+ return Math.floor(now() / 1000);
352
415
  }
353
416
  async function deleteExpiredKeyPackages(kysely, ownerDID) {
354
417
  await kysely.deleteFrom('kubun_hub_key_packages').where('owner_did', '=', ownerDID).where('not_after', 'is not', null).where('not_after', '<=', nowSeconds()).execute();
@@ -427,7 +490,7 @@ export function createHubStore(params) {
427
490
  await kysely.insertInto('kubun_hub_subscriptions').values({
428
491
  topic_id: topicID,
429
492
  subscriber_did: subscriberDID,
430
- last_active_at: Date.now(),
493
+ last_active_at: now(),
431
494
  retention
432
495
  }).onConflict((oc)=>oc.columns([
433
496
  'topic_id',
@@ -464,7 +527,7 @@ export function createHubStore(params) {
464
527
  const kysely = await db();
465
528
  // Strict `<`: a row refreshed exactly at the boundary survives. Subscription
466
529
  // retention runs on a much longer window than message purge.
467
- const threshold = Date.now() - purgeParams.olderThan * 1000;
530
+ const threshold = now() - purgeParams.olderThan * 1000;
468
531
  // Atomic delete-and-return so the reported set exactly matches the rows
469
532
  // actually removed — a subscriber who rejoins between evaluating and deleting
470
533
  // is neither reaped nor over-reported. Mirrors `fetchKeyPackages`.
package/lib/index.js CHANGED
@@ -15,6 +15,8 @@ export async function createHub(options) {
15
15
  auth: options.auth,
16
16
  allowedOrigin: options.allowedOrigin,
17
17
  rateLimits: options.rateLimits,
18
+ maxRequestBodySize: options.maxRequestBodySize,
19
+ maxSessionBufferBytes: options.maxSessionBufferBytes,
18
20
  retention: options.retention,
19
21
  observers: options.observers,
20
22
  ownsKysely: false
package/lib/migrations.js CHANGED
@@ -31,7 +31,7 @@ export function createHubMigrations(ctx) {
31
31
  // Stored per-topic head: the sequenceID of the last accepted log publish.
32
32
  // Stored state, NOT a projection — it outlives every frame, so a trim or
33
33
  // purge that empties the log leaves it standing.
34
- await db.schema.createTable('kubun_hub_topics').ifNotExists().addColumn('topic_id', t.text, (col)=>col.notNull().primaryKey()).addColumn('head', t.text, (col)=>col.notNull()).execute();
34
+ await db.schema.createTable('kubun_hub_topics').ifNotExists().addColumn('topic_id', t.text, (col)=>col.notNull().primaryKey()).addColumn('head', t.text, (col)=>col.notNull()).addColumn('removed_through', t.text).execute();
35
35
  // Persistent, global, monotonic sequence counter. Persisted (not derived
36
36
  // from surviving frames) so trimming or purging a topic to empty never
37
37
  // regresses minting below a head the topic still names. Global so a
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kubun/hub",
3
- "version": "0.17.2",
3
+ "version": "0.18.0",
4
4
  "license": "see LICENSE.md",
5
5
  "sideEffects": false,
6
6
  "type": "module",
@@ -14,32 +14,32 @@
14
14
  "LICENSE.md"
15
15
  ],
16
16
  "dependencies": {
17
- "@enkaku/http-serve": "^0.21.0",
18
- "@enkaku/server": "^0.21.2",
19
- "@hono/node-server": "^2.1.1",
20
- "@kokuin/token": "^0.5.0",
21
- "@kubun/db-adapter": "^0.17.0",
22
- "@kubun/db-provider": "^0.17.0",
23
- "@kubun/http-util": "^0.17.0",
24
- "@kumiai/hub-protocol": "^0.9.0",
25
- "@kumiai/hub-server": "^0.9.0",
26
- "@sozai/event": "^0.1.3",
17
+ "@enkaku/http-serve": "^0.21.5",
18
+ "@enkaku/server": "^0.21.5",
19
+ "@hono/node-server": "^2.1.5",
20
+ "@kokuin/token": "^0.5.1",
21
+ "@kubun/db-adapter": "^0.18.0",
22
+ "@kubun/db-provider": "^0.18.0",
23
+ "@kubun/http-util": "^0.18.0",
24
+ "@kumiai/hub-protocol": "^0.10.2",
25
+ "@kumiai/hub-server": "^0.10.4",
26
+ "@sozai/event": "^0.1.6",
27
27
  "get-port": "^7.2.0",
28
- "hono": "^4.13.7",
29
- "kysely": "^0.29.5"
28
+ "hono": "^4.13.13",
29
+ "kysely": "^0.29.7"
30
30
  },
31
31
  "devDependencies": {
32
- "@enkaku/client": "^0.21.2",
33
- "@enkaku/http-fetch": "^0.21.0",
34
- "@enkaku/protocol": "^0.21.1",
35
- "@enkaku/transport": "^0.21.0",
36
- "@kubun/db-better-sqlite": "^0.17.0",
37
- "@kubun/db-node-sqlite": "^0.17.0",
38
- "@kubun/db-postgres": "^0.17.0",
39
- "@kumiai/hub-conformance": "^0.9.0",
40
- "@kumiai/mls": "^0.9.0",
41
- "@sozai/async": "^0.2.1",
42
- "@testcontainers/postgresql": "^12.1.0"
32
+ "@enkaku/client": "^0.21.5",
33
+ "@enkaku/http-fetch": "^0.21.5",
34
+ "@enkaku/protocol": "^0.21.5",
35
+ "@enkaku/transport": "^0.21.5",
36
+ "@kubun/db-better-sqlite": "^0.18.0",
37
+ "@kubun/db-node-sqlite": "^0.18.0",
38
+ "@kubun/db-postgres": "^0.18.0",
39
+ "@kumiai/hub-conformance": "^0.10.1",
40
+ "@kumiai/mls": "^0.10.9",
41
+ "@sozai/async": "^0.4.1",
42
+ "@testcontainers/postgresql": "^12.2.1"
43
43
  },
44
44
  "publishConfig": {
45
45
  "access": "public"
@@ -49,6 +49,9 @@
49
49
  "build:clean": "del lib",
50
50
  "build:js": "del 'lib/**/*.js' && swc src -d ./lib --config-file ../../node_modules/@kigu/dev/swc.json --strip-leading-paths",
51
51
  "build:types": "del 'lib/**/*.d.ts' 'lib/**/*.d.ts.map' && tsc --emitDeclarationOnly --skipLibCheck",
52
- "test:types": "tsc --noEmit -p tsconfig.test.json"
52
+ "build:types:ci": "tsc --emitDeclarationOnly --declarationMap false",
53
+ "test": "pnpm run test:types && pnpm run test:unit",
54
+ "test:types": "tsc --noEmit",
55
+ "test:unit": "vitest run"
53
56
  }
54
57
  }