@kubun/hub 0.17.3 → 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.
- package/lib/hub-store.d.ts +3 -1
- package/lib/hub-store.js +133 -70
- package/lib/migrations.js +1 -1
- package/package.json +28 -25
package/lib/hub-store.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
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(
|
|
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
|
|
46
|
-
|
|
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:
|
|
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:
|
|
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
|
-
//
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
'sequence_id',
|
|
256
|
-
|
|
257
|
-
'
|
|
258
|
-
'
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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
|
|
300
|
-
//
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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
|
|
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', '<=',
|
|
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
|
-
|
|
334
|
-
const
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
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(
|
|
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:
|
|
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 =
|
|
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/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.
|
|
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.
|
|
18
|
-
"@enkaku/server": "^0.21.
|
|
19
|
-
"@hono/node-server": "^2.1.
|
|
20
|
-
"@kokuin/token": "^0.5.
|
|
21
|
-
"@kubun/db-adapter": "^0.
|
|
22
|
-
"@kubun/db-provider": "^0.
|
|
23
|
-
"@kubun/http-util": "^0.
|
|
24
|
-
"@kumiai/hub-protocol": "^0.
|
|
25
|
-
"@kumiai/hub-server": "^0.
|
|
26
|
-
"@sozai/event": "^0.1.
|
|
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.
|
|
29
|
-
"kysely": "^0.29.
|
|
28
|
+
"hono": "^4.13.13",
|
|
29
|
+
"kysely": "^0.29.7"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
|
-
"@enkaku/client": "^0.21.
|
|
33
|
-
"@enkaku/http-fetch": "^0.21.
|
|
34
|
-
"@enkaku/protocol": "^0.21.
|
|
35
|
-
"@enkaku/transport": "^0.21.
|
|
36
|
-
"@kubun/db-better-sqlite": "^0.
|
|
37
|
-
"@kubun/db-node-sqlite": "^0.
|
|
38
|
-
"@kubun/db-postgres": "^0.
|
|
39
|
-
"@kumiai/hub-conformance": "^0.
|
|
40
|
-
"@kumiai/mls": "^0.9
|
|
41
|
-
"@sozai/async": "^0.
|
|
42
|
-
"@testcontainers/postgresql": "^12.1
|
|
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
|
-
"
|
|
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
|
}
|