@fragno-dev/db 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +20 -20
- package/CHANGELOG.md +17 -0
- package/dist/db-fragment-definition-builder.d.ts +55 -1
- package/dist/db-fragment-definition-builder.d.ts.map +1 -1
- package/dist/db-fragment-definition-builder.js +49 -5
- package/dist/db-fragment-definition-builder.js.map +1 -1
- package/dist/fragments/internal-fragment.d.ts.map +1 -1
- package/dist/fragments/internal-fragment.js.map +1 -1
- package/dist/mod.d.ts.map +1 -1
- package/dist/query/unit-of-work/execute-unit-of-work.d.ts +37 -1
- package/dist/query/unit-of-work/execute-unit-of-work.d.ts.map +1 -1
- package/dist/query/unit-of-work/execute-unit-of-work.js +133 -1
- package/dist/query/unit-of-work/execute-unit-of-work.js.map +1 -1
- package/dist/query/unit-of-work/unit-of-work.d.ts +18 -3
- package/dist/query/unit-of-work/unit-of-work.d.ts.map +1 -1
- package/dist/query/unit-of-work/unit-of-work.js +25 -11
- package/dist/query/unit-of-work/unit-of-work.js.map +1 -1
- package/dist/schema/create.d.ts +0 -3
- package/dist/schema/create.d.ts.map +1 -1
- package/dist/schema/create.js +0 -4
- package/dist/schema/create.js.map +1 -1
- package/dist/sql-driver/dialects/durable-object-dialect.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/adapters/drizzle/drizzle-adapter-pglite.test.ts +1 -0
- package/src/db-fragment-definition-builder.ts +187 -7
- package/src/fragments/internal-fragment.ts +0 -1
- package/src/hooks/hooks.test.ts +28 -16
- package/src/query/unit-of-work/execute-unit-of-work.test.ts +555 -1
- package/src/query/unit-of-work/execute-unit-of-work.ts +312 -4
- package/src/query/unit-of-work/unit-of-work-coordinator.test.ts +249 -1
- package/src/query/unit-of-work/unit-of-work.ts +39 -17
- package/src/schema/create.ts +0 -5
|
@@ -3,11 +3,18 @@ import { schema, idColumn, FragnoId } from "../../schema/create";
|
|
|
3
3
|
import {
|
|
4
4
|
createUnitOfWork,
|
|
5
5
|
type TypedUnitOfWork,
|
|
6
|
+
type IUnitOfWork,
|
|
6
7
|
type UOWCompiler,
|
|
7
8
|
type UOWDecoder,
|
|
8
9
|
type UOWExecutor,
|
|
9
10
|
} from "./unit-of-work";
|
|
10
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
executeUnitOfWork,
|
|
13
|
+
executeRestrictedUnitOfWork,
|
|
14
|
+
executeTxArray,
|
|
15
|
+
executeTxCallbacks,
|
|
16
|
+
executeServiceTx,
|
|
17
|
+
} from "./execute-unit-of-work";
|
|
11
18
|
import {
|
|
12
19
|
ExponentialBackoffRetryPolicy,
|
|
13
20
|
LinearBackoffRetryPolicy,
|
|
@@ -1307,4 +1314,551 @@ describe("executeRestrictedUnitOfWork", () => {
|
|
|
1307
1314
|
expect(await deferred.promise).toContain('relation "settings" does not exist');
|
|
1308
1315
|
});
|
|
1309
1316
|
});
|
|
1317
|
+
|
|
1318
|
+
describe("executeTxArray", () => {
|
|
1319
|
+
it("should execute multiple service promises and await them before mutations", async () => {
|
|
1320
|
+
const compiler = createMockCompiler();
|
|
1321
|
+
const executor: UOWExecutor<unknown, unknown> = {
|
|
1322
|
+
executeRetrievalPhase: async () => [],
|
|
1323
|
+
executeMutationPhase: async () => ({
|
|
1324
|
+
success: true,
|
|
1325
|
+
createdInternalIds: [],
|
|
1326
|
+
}),
|
|
1327
|
+
};
|
|
1328
|
+
const decoder = createMockDecoder();
|
|
1329
|
+
|
|
1330
|
+
let retrievalExecuted = false;
|
|
1331
|
+
let servicesResolved = false;
|
|
1332
|
+
|
|
1333
|
+
const result = await executeTxArray(
|
|
1334
|
+
() => [
|
|
1335
|
+
Promise.resolve().then(async () => {
|
|
1336
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
1337
|
+
servicesResolved = true;
|
|
1338
|
+
return { result1: "value1" };
|
|
1339
|
+
}),
|
|
1340
|
+
Promise.resolve().then(async () => {
|
|
1341
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
1342
|
+
return { result2: "value2" };
|
|
1343
|
+
}),
|
|
1344
|
+
],
|
|
1345
|
+
{
|
|
1346
|
+
createUnitOfWork: () => {
|
|
1347
|
+
const uow = createUnitOfWork(compiler, executor, decoder);
|
|
1348
|
+
const originalExecuteRetrieve = uow.executeRetrieve.bind(uow);
|
|
1349
|
+
uow.executeRetrieve = async () => {
|
|
1350
|
+
retrievalExecuted = true;
|
|
1351
|
+
return originalExecuteRetrieve();
|
|
1352
|
+
};
|
|
1353
|
+
return uow;
|
|
1354
|
+
},
|
|
1355
|
+
},
|
|
1356
|
+
);
|
|
1357
|
+
|
|
1358
|
+
expect(retrievalExecuted).toBe(true);
|
|
1359
|
+
expect(servicesResolved).toBe(true);
|
|
1360
|
+
expect(result).toEqual([{ result1: "value1" }, { result2: "value2" }]);
|
|
1361
|
+
});
|
|
1362
|
+
|
|
1363
|
+
it("should retry on concurrency conflict", async () => {
|
|
1364
|
+
const compiler = createMockCompiler();
|
|
1365
|
+
let attemptCount = 0;
|
|
1366
|
+
const executor: UOWExecutor<unknown, unknown> = {
|
|
1367
|
+
executeRetrievalPhase: async () => [],
|
|
1368
|
+
executeMutationPhase: async () => {
|
|
1369
|
+
attemptCount++;
|
|
1370
|
+
if (attemptCount < 2) {
|
|
1371
|
+
return { success: false };
|
|
1372
|
+
}
|
|
1373
|
+
return {
|
|
1374
|
+
success: true,
|
|
1375
|
+
createdInternalIds: [],
|
|
1376
|
+
};
|
|
1377
|
+
},
|
|
1378
|
+
};
|
|
1379
|
+
const decoder = createMockDecoder();
|
|
1380
|
+
|
|
1381
|
+
const result = await executeTxArray(() => [Promise.resolve({ result: "value" })], {
|
|
1382
|
+
createUnitOfWork: () => createUnitOfWork(compiler, executor, decoder),
|
|
1383
|
+
retryPolicy: new ExponentialBackoffRetryPolicy({ maxRetries: 3, initialDelayMs: 1 }),
|
|
1384
|
+
});
|
|
1385
|
+
|
|
1386
|
+
expect(attemptCount).toBe(2);
|
|
1387
|
+
expect(result).toEqual([{ result: "value" }]);
|
|
1388
|
+
});
|
|
1389
|
+
|
|
1390
|
+
it("should throw if retries exhausted", async () => {
|
|
1391
|
+
const compiler = createMockCompiler();
|
|
1392
|
+
const executor: UOWExecutor<unknown, unknown> = {
|
|
1393
|
+
executeRetrievalPhase: async () => [],
|
|
1394
|
+
executeMutationPhase: async () => ({ success: false }),
|
|
1395
|
+
};
|
|
1396
|
+
const decoder = createMockDecoder();
|
|
1397
|
+
|
|
1398
|
+
await expect(
|
|
1399
|
+
executeTxArray(() => [Promise.resolve({ result: "value" })], {
|
|
1400
|
+
createUnitOfWork: () => createUnitOfWork(compiler, executor, decoder),
|
|
1401
|
+
retryPolicy: new NoRetryPolicy(),
|
|
1402
|
+
}),
|
|
1403
|
+
).rejects.toThrow("optimistic concurrency conflict");
|
|
1404
|
+
});
|
|
1405
|
+
});
|
|
1406
|
+
|
|
1407
|
+
describe("executeTxCallbacks", () => {
|
|
1408
|
+
it("should execute retrieve and mutate callbacks in order", async () => {
|
|
1409
|
+
const compiler = createMockCompiler();
|
|
1410
|
+
const executor: UOWExecutor<unknown, unknown> = {
|
|
1411
|
+
executeRetrievalPhase: async () => [],
|
|
1412
|
+
executeMutationPhase: async () => ({
|
|
1413
|
+
success: true,
|
|
1414
|
+
createdInternalIds: [],
|
|
1415
|
+
}),
|
|
1416
|
+
};
|
|
1417
|
+
const decoder = createMockDecoder();
|
|
1418
|
+
|
|
1419
|
+
const executionOrder: string[] = [];
|
|
1420
|
+
|
|
1421
|
+
const result = await executeTxCallbacks(
|
|
1422
|
+
{
|
|
1423
|
+
retrieve: ({ forSchema }) => {
|
|
1424
|
+
executionOrder.push("retrieve");
|
|
1425
|
+
const uow = forSchema(testSchema);
|
|
1426
|
+
uow.find("users", (b) => b.whereIndex("idx_email"));
|
|
1427
|
+
return { servicePromise: Promise.resolve({ value: "result" }) };
|
|
1428
|
+
},
|
|
1429
|
+
mutate: ({ forSchema }, { servicePromise }) => {
|
|
1430
|
+
executionOrder.push("mutate");
|
|
1431
|
+
const uow = forSchema(testSchema);
|
|
1432
|
+
uow.create("users", { email: "test@example.com", name: "Test", balance: 0 });
|
|
1433
|
+
return servicePromise;
|
|
1434
|
+
},
|
|
1435
|
+
},
|
|
1436
|
+
{
|
|
1437
|
+
createUnitOfWork: () => createUnitOfWork(compiler, executor, decoder),
|
|
1438
|
+
},
|
|
1439
|
+
);
|
|
1440
|
+
|
|
1441
|
+
expect(executionOrder).toEqual(["retrieve", "mutate"]);
|
|
1442
|
+
expect(result).toEqual({ value: "result" });
|
|
1443
|
+
});
|
|
1444
|
+
|
|
1445
|
+
it("should handle retrieve-only transactions", async () => {
|
|
1446
|
+
const compiler = createMockCompiler();
|
|
1447
|
+
const executor: UOWExecutor<unknown, unknown> = {
|
|
1448
|
+
executeRetrievalPhase: async () => [
|
|
1449
|
+
[
|
|
1450
|
+
{
|
|
1451
|
+
id: FragnoId.fromExternal("1", 1),
|
|
1452
|
+
email: "test@example.com",
|
|
1453
|
+
name: "Test",
|
|
1454
|
+
balance: 0,
|
|
1455
|
+
},
|
|
1456
|
+
],
|
|
1457
|
+
],
|
|
1458
|
+
executeMutationPhase: async () => ({
|
|
1459
|
+
success: true,
|
|
1460
|
+
createdInternalIds: [],
|
|
1461
|
+
}),
|
|
1462
|
+
};
|
|
1463
|
+
const decoder = createMockDecoder();
|
|
1464
|
+
|
|
1465
|
+
const result = await executeTxCallbacks(
|
|
1466
|
+
{
|
|
1467
|
+
retrieve: ({ forSchema }) => {
|
|
1468
|
+
const uow = forSchema(testSchema);
|
|
1469
|
+
uow.find("users", (b) => b.whereIndex("idx_email"));
|
|
1470
|
+
return { users: [] };
|
|
1471
|
+
},
|
|
1472
|
+
},
|
|
1473
|
+
{
|
|
1474
|
+
createUnitOfWork: () => createUnitOfWork(compiler, executor, decoder),
|
|
1475
|
+
},
|
|
1476
|
+
);
|
|
1477
|
+
|
|
1478
|
+
expect(result).toEqual({ users: [] });
|
|
1479
|
+
});
|
|
1480
|
+
|
|
1481
|
+
it("should handle mutate-only transactions", async () => {
|
|
1482
|
+
const compiler = createMockCompiler();
|
|
1483
|
+
const executor: UOWExecutor<unknown, unknown> = {
|
|
1484
|
+
executeRetrievalPhase: async () => [],
|
|
1485
|
+
executeMutationPhase: async () => ({
|
|
1486
|
+
success: true,
|
|
1487
|
+
createdInternalIds: [BigInt(1)],
|
|
1488
|
+
}),
|
|
1489
|
+
};
|
|
1490
|
+
const decoder = createMockDecoder();
|
|
1491
|
+
|
|
1492
|
+
const result = await executeTxCallbacks(
|
|
1493
|
+
{
|
|
1494
|
+
mutate: ({ forSchema }) => {
|
|
1495
|
+
const uow = forSchema(testSchema);
|
|
1496
|
+
uow.create("users", { email: "test@example.com", name: "Test", balance: 0 });
|
|
1497
|
+
return { created: true };
|
|
1498
|
+
},
|
|
1499
|
+
},
|
|
1500
|
+
{
|
|
1501
|
+
createUnitOfWork: () => createUnitOfWork(compiler, executor, decoder),
|
|
1502
|
+
},
|
|
1503
|
+
);
|
|
1504
|
+
|
|
1505
|
+
expect(result).toEqual({ created: true });
|
|
1506
|
+
});
|
|
1507
|
+
|
|
1508
|
+
it("should await promises returned from mutate callback", async () => {
|
|
1509
|
+
const compiler = createMockCompiler();
|
|
1510
|
+
const executor: UOWExecutor<unknown, unknown> = {
|
|
1511
|
+
executeRetrievalPhase: async () => [],
|
|
1512
|
+
executeMutationPhase: async () => ({
|
|
1513
|
+
success: true,
|
|
1514
|
+
createdInternalIds: [],
|
|
1515
|
+
}),
|
|
1516
|
+
};
|
|
1517
|
+
const decoder = createMockDecoder();
|
|
1518
|
+
|
|
1519
|
+
const result = await executeTxCallbacks(
|
|
1520
|
+
{
|
|
1521
|
+
mutate: () => {
|
|
1522
|
+
return Promise.resolve({ value: "async result" });
|
|
1523
|
+
},
|
|
1524
|
+
},
|
|
1525
|
+
{
|
|
1526
|
+
createUnitOfWork: () => createUnitOfWork(compiler, executor, decoder),
|
|
1527
|
+
},
|
|
1528
|
+
);
|
|
1529
|
+
|
|
1530
|
+
expect(result).toEqual({ value: "async result" });
|
|
1531
|
+
});
|
|
1532
|
+
|
|
1533
|
+
it("should retry on concurrency conflict", async () => {
|
|
1534
|
+
const compiler = createMockCompiler();
|
|
1535
|
+
let attemptCount = 0;
|
|
1536
|
+
const executor: UOWExecutor<unknown, unknown> = {
|
|
1537
|
+
executeRetrievalPhase: async () => [],
|
|
1538
|
+
executeMutationPhase: async () => {
|
|
1539
|
+
attemptCount++;
|
|
1540
|
+
if (attemptCount < 2) {
|
|
1541
|
+
return { success: false };
|
|
1542
|
+
}
|
|
1543
|
+
return {
|
|
1544
|
+
success: true,
|
|
1545
|
+
createdInternalIds: [],
|
|
1546
|
+
};
|
|
1547
|
+
},
|
|
1548
|
+
};
|
|
1549
|
+
const decoder = createMockDecoder();
|
|
1550
|
+
|
|
1551
|
+
const result = await executeTxCallbacks(
|
|
1552
|
+
{
|
|
1553
|
+
mutate: () => ({ value: "result" }),
|
|
1554
|
+
},
|
|
1555
|
+
{
|
|
1556
|
+
createUnitOfWork: () => createUnitOfWork(compiler, executor, decoder),
|
|
1557
|
+
retryPolicy: new ExponentialBackoffRetryPolicy({ maxRetries: 3, initialDelayMs: 1 }),
|
|
1558
|
+
},
|
|
1559
|
+
);
|
|
1560
|
+
|
|
1561
|
+
expect(attemptCount).toBe(2);
|
|
1562
|
+
expect(result).toEqual({ value: "result" });
|
|
1563
|
+
});
|
|
1564
|
+
});
|
|
1565
|
+
|
|
1566
|
+
describe("executeServiceTx", () => {
|
|
1567
|
+
it("should execute service transaction with retrieve and mutate", async () => {
|
|
1568
|
+
const compiler = createMockCompiler();
|
|
1569
|
+
const executor: UOWExecutor<unknown, unknown> = {
|
|
1570
|
+
executeRetrievalPhase: async () => [
|
|
1571
|
+
[
|
|
1572
|
+
{
|
|
1573
|
+
id: FragnoId.fromExternal("1", 1),
|
|
1574
|
+
email: "test@example.com",
|
|
1575
|
+
name: "Test",
|
|
1576
|
+
balance: 100,
|
|
1577
|
+
},
|
|
1578
|
+
],
|
|
1579
|
+
],
|
|
1580
|
+
executeMutationPhase: async () => ({
|
|
1581
|
+
success: true,
|
|
1582
|
+
createdInternalIds: [],
|
|
1583
|
+
}),
|
|
1584
|
+
};
|
|
1585
|
+
const decoder = createMockDecoder();
|
|
1586
|
+
|
|
1587
|
+
const baseUow = createUnitOfWork(compiler, executor, decoder);
|
|
1588
|
+
const restrictedUow = baseUow.restrict();
|
|
1589
|
+
|
|
1590
|
+
// Start service tx
|
|
1591
|
+
const servicePromise = executeServiceTx(
|
|
1592
|
+
testSchema,
|
|
1593
|
+
{
|
|
1594
|
+
retrieve: (uow) => {
|
|
1595
|
+
return uow.findFirst("users", (b) => b.whereIndex("idx_email"));
|
|
1596
|
+
},
|
|
1597
|
+
mutate: async (uow, [user]) => {
|
|
1598
|
+
if (!user) {
|
|
1599
|
+
return { ok: false };
|
|
1600
|
+
}
|
|
1601
|
+
await new Promise((resolve) => setTimeout(resolve, 10)); // Async work
|
|
1602
|
+
uow.update("users", user.id, (b) => b.set({ balance: user.balance - 10 }));
|
|
1603
|
+
return { ok: true, newBalance: user.balance - 10 };
|
|
1604
|
+
},
|
|
1605
|
+
},
|
|
1606
|
+
restrictedUow,
|
|
1607
|
+
);
|
|
1608
|
+
|
|
1609
|
+
// Simulate handler executing phases concurrently with service
|
|
1610
|
+
// Yield to let service start awaiting retrievalPhase
|
|
1611
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
1612
|
+
|
|
1613
|
+
// Execute retrieve phase
|
|
1614
|
+
await baseUow.executeRetrieve();
|
|
1615
|
+
|
|
1616
|
+
// Wait for service mutate callback to schedule mutations
|
|
1617
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
1618
|
+
|
|
1619
|
+
// Execute mutation phase
|
|
1620
|
+
await baseUow.executeMutations();
|
|
1621
|
+
|
|
1622
|
+
// Wait for service to complete
|
|
1623
|
+
const serviceResult = await servicePromise;
|
|
1624
|
+
expect(serviceResult).toEqual({ ok: true, newBalance: 90 });
|
|
1625
|
+
});
|
|
1626
|
+
|
|
1627
|
+
it("should handle async mutate callback", async () => {
|
|
1628
|
+
const compiler = createMockCompiler();
|
|
1629
|
+
const executor: UOWExecutor<unknown, unknown> = {
|
|
1630
|
+
executeRetrievalPhase: async () => [[]],
|
|
1631
|
+
executeMutationPhase: async () => ({
|
|
1632
|
+
success: true,
|
|
1633
|
+
createdInternalIds: [BigInt(1)],
|
|
1634
|
+
}),
|
|
1635
|
+
};
|
|
1636
|
+
const decoder = createMockDecoder();
|
|
1637
|
+
|
|
1638
|
+
const baseUow = createUnitOfWork(compiler, executor, decoder);
|
|
1639
|
+
const restrictedUow = baseUow.restrict();
|
|
1640
|
+
|
|
1641
|
+
// Simulate handler executing phases concurrently with service
|
|
1642
|
+
const handlerSimulation = (async () => {
|
|
1643
|
+
// Yield to let service start
|
|
1644
|
+
await Promise.resolve();
|
|
1645
|
+
// Execute retrieve phase
|
|
1646
|
+
await baseUow.executeRetrieve();
|
|
1647
|
+
// Wait for service mutate callback to schedule mutations
|
|
1648
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
1649
|
+
// Execute mutation phase
|
|
1650
|
+
await baseUow.executeMutations();
|
|
1651
|
+
})();
|
|
1652
|
+
|
|
1653
|
+
// Start service tx
|
|
1654
|
+
const servicePromise = executeServiceTx(
|
|
1655
|
+
testSchema,
|
|
1656
|
+
{
|
|
1657
|
+
retrieve: (uow) => {
|
|
1658
|
+
return uow.find("users", (b) => b.whereIndex("idx_email"));
|
|
1659
|
+
},
|
|
1660
|
+
mutate: async (uow) => {
|
|
1661
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
1662
|
+
uow.create("users", { email: "new@example.com", name: "New", balance: 0 });
|
|
1663
|
+
return { created: true };
|
|
1664
|
+
},
|
|
1665
|
+
},
|
|
1666
|
+
restrictedUow,
|
|
1667
|
+
);
|
|
1668
|
+
|
|
1669
|
+
// Wait for both handler and service to complete
|
|
1670
|
+
await Promise.all([handlerSimulation, servicePromise]);
|
|
1671
|
+
|
|
1672
|
+
const serviceResult = await servicePromise;
|
|
1673
|
+
expect(serviceResult).toEqual({ created: true });
|
|
1674
|
+
});
|
|
1675
|
+
|
|
1676
|
+
it("should prevent anti-pattern: service async work completes before mutations execute", async () => {
|
|
1677
|
+
const compiler = createMockCompiler();
|
|
1678
|
+
const executor: UOWExecutor<unknown, unknown> = {
|
|
1679
|
+
executeRetrievalPhase: async () => [[]],
|
|
1680
|
+
executeMutationPhase: async () => ({
|
|
1681
|
+
success: true,
|
|
1682
|
+
createdInternalIds: [BigInt(1)],
|
|
1683
|
+
}),
|
|
1684
|
+
};
|
|
1685
|
+
const decoder = createMockDecoder();
|
|
1686
|
+
|
|
1687
|
+
const baseUow = createUnitOfWork(compiler, executor, decoder);
|
|
1688
|
+
const restrictedUow = baseUow.restrict();
|
|
1689
|
+
|
|
1690
|
+
let asyncWorkCompleted = false;
|
|
1691
|
+
let mutationScheduled = false;
|
|
1692
|
+
|
|
1693
|
+
// Simulate handler executing phases concurrently with service
|
|
1694
|
+
const handlerSimulation = (async () => {
|
|
1695
|
+
// Yield to let service start
|
|
1696
|
+
await Promise.resolve();
|
|
1697
|
+
// Execute retrieve phase
|
|
1698
|
+
await baseUow.executeRetrieve();
|
|
1699
|
+
// Wait for service mutate callback to schedule mutations (including async work)
|
|
1700
|
+
await new Promise((resolve) => setTimeout(resolve, 30));
|
|
1701
|
+
// Execute mutation phase
|
|
1702
|
+
await baseUow.executeMutations();
|
|
1703
|
+
})();
|
|
1704
|
+
|
|
1705
|
+
// Start service tx
|
|
1706
|
+
const servicePromise = executeServiceTx(
|
|
1707
|
+
testSchema,
|
|
1708
|
+
{
|
|
1709
|
+
retrieve: (uow) => {
|
|
1710
|
+
return uow.find("users", (b) => b.whereIndex("idx_email"));
|
|
1711
|
+
},
|
|
1712
|
+
mutate: async (uow) => {
|
|
1713
|
+
// Simulate async work (like hashing backup codes)
|
|
1714
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
1715
|
+
asyncWorkCompleted = true;
|
|
1716
|
+
|
|
1717
|
+
// Schedule mutation
|
|
1718
|
+
uow.create("users", { email: "test@example.com", name: "Test", balance: 0 });
|
|
1719
|
+
mutationScheduled = true;
|
|
1720
|
+
return { success: true };
|
|
1721
|
+
},
|
|
1722
|
+
},
|
|
1723
|
+
restrictedUow,
|
|
1724
|
+
);
|
|
1725
|
+
|
|
1726
|
+
// Wait for both handler and service to complete
|
|
1727
|
+
await Promise.all([handlerSimulation, servicePromise]);
|
|
1728
|
+
|
|
1729
|
+
expect(asyncWorkCompleted).toBe(true);
|
|
1730
|
+
expect(mutationScheduled).toBe(true);
|
|
1731
|
+
});
|
|
1732
|
+
});
|
|
1733
|
+
|
|
1734
|
+
describe("executeTxArray with executeServiceTx", () => {
|
|
1735
|
+
it("should execute a single service promise created with executeServiceTx", async () => {
|
|
1736
|
+
const compiler = createMockCompiler();
|
|
1737
|
+
const executor: UOWExecutor<unknown, unknown> = {
|
|
1738
|
+
executeRetrievalPhase: async () => [
|
|
1739
|
+
[
|
|
1740
|
+
{
|
|
1741
|
+
id: FragnoId.fromExternal("1", 1),
|
|
1742
|
+
email: "user1@example.com",
|
|
1743
|
+
name: "User 1",
|
|
1744
|
+
balance: 100,
|
|
1745
|
+
},
|
|
1746
|
+
],
|
|
1747
|
+
],
|
|
1748
|
+
executeMutationPhase: async () => ({
|
|
1749
|
+
success: true,
|
|
1750
|
+
createdInternalIds: [],
|
|
1751
|
+
}),
|
|
1752
|
+
};
|
|
1753
|
+
const decoder = createMockDecoder();
|
|
1754
|
+
|
|
1755
|
+
let currentUow: IUnitOfWork | null = null;
|
|
1756
|
+
|
|
1757
|
+
// Execute the service promise using executeTxArray
|
|
1758
|
+
const result = await executeTxArray(
|
|
1759
|
+
() => [
|
|
1760
|
+
executeServiceTx(
|
|
1761
|
+
testSchema,
|
|
1762
|
+
{
|
|
1763
|
+
retrieve: (uow) => {
|
|
1764
|
+
return uow.findFirst("users", (b) => b.whereIndex("idx_email"));
|
|
1765
|
+
},
|
|
1766
|
+
mutate: async (uow, [user]) => {
|
|
1767
|
+
if (!user) {
|
|
1768
|
+
return { ok: false };
|
|
1769
|
+
}
|
|
1770
|
+
// simulate async work
|
|
1771
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
1772
|
+
|
|
1773
|
+
uow.update("users", user.id, (b) => b.set({ balance: user.balance + 50 }));
|
|
1774
|
+
return { ok: true, newBalance: user.balance + 50 };
|
|
1775
|
+
},
|
|
1776
|
+
},
|
|
1777
|
+
currentUow!,
|
|
1778
|
+
),
|
|
1779
|
+
],
|
|
1780
|
+
{
|
|
1781
|
+
createUnitOfWork: () => {
|
|
1782
|
+
currentUow = createUnitOfWork(compiler, executor, decoder);
|
|
1783
|
+
return currentUow;
|
|
1784
|
+
},
|
|
1785
|
+
},
|
|
1786
|
+
);
|
|
1787
|
+
|
|
1788
|
+
expect(result).toHaveLength(1);
|
|
1789
|
+
expect(result[0]).toEqual({ ok: true, newBalance: 150 });
|
|
1790
|
+
});
|
|
1791
|
+
|
|
1792
|
+
it("should retry and eventually succeed when mutations fail on first attempts", async () => {
|
|
1793
|
+
const compiler = createMockCompiler();
|
|
1794
|
+
let executionAttemptCount = 0;
|
|
1795
|
+
let factoryCallCount = 0;
|
|
1796
|
+
|
|
1797
|
+
const executor: UOWExecutor<unknown, unknown> = {
|
|
1798
|
+
executeRetrievalPhase: async () => [
|
|
1799
|
+
[
|
|
1800
|
+
{
|
|
1801
|
+
id: FragnoId.fromExternal("1", 1),
|
|
1802
|
+
email: "user1@example.com",
|
|
1803
|
+
name: "User 1",
|
|
1804
|
+
balance: 100,
|
|
1805
|
+
},
|
|
1806
|
+
],
|
|
1807
|
+
],
|
|
1808
|
+
executeMutationPhase: async () => {
|
|
1809
|
+
executionAttemptCount++;
|
|
1810
|
+
// Fail on first 2 attempts, succeed on 3rd
|
|
1811
|
+
if (executionAttemptCount < 3) {
|
|
1812
|
+
return { success: false };
|
|
1813
|
+
}
|
|
1814
|
+
return {
|
|
1815
|
+
success: true,
|
|
1816
|
+
createdInternalIds: [],
|
|
1817
|
+
};
|
|
1818
|
+
},
|
|
1819
|
+
};
|
|
1820
|
+
const decoder = createMockDecoder();
|
|
1821
|
+
|
|
1822
|
+
let currentUow: IUnitOfWork | null = null;
|
|
1823
|
+
|
|
1824
|
+
const result = await executeTxArray(
|
|
1825
|
+
() => {
|
|
1826
|
+
factoryCallCount++;
|
|
1827
|
+
return [
|
|
1828
|
+
executeServiceTx(
|
|
1829
|
+
testSchema,
|
|
1830
|
+
{
|
|
1831
|
+
retrieve: (uow) => {
|
|
1832
|
+
return uow.findFirst("users", (b) => b.whereIndex("idx_email"));
|
|
1833
|
+
},
|
|
1834
|
+
mutate: async (uow, [user]) => {
|
|
1835
|
+
if (!user) {
|
|
1836
|
+
return { ok: false };
|
|
1837
|
+
}
|
|
1838
|
+
// simulate async work
|
|
1839
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
1840
|
+
|
|
1841
|
+
uow.update("users", user.id, (b) => b.set({ balance: user.balance + 50 }));
|
|
1842
|
+
return { ok: true, newBalance: user.balance + 50, attempt: factoryCallCount };
|
|
1843
|
+
},
|
|
1844
|
+
},
|
|
1845
|
+
currentUow!,
|
|
1846
|
+
),
|
|
1847
|
+
];
|
|
1848
|
+
},
|
|
1849
|
+
{
|
|
1850
|
+
createUnitOfWork: () => {
|
|
1851
|
+
currentUow = createUnitOfWork(compiler, executor, decoder);
|
|
1852
|
+
return currentUow;
|
|
1853
|
+
},
|
|
1854
|
+
retryPolicy: new ExponentialBackoffRetryPolicy({ maxRetries: 3, initialDelayMs: 1 }),
|
|
1855
|
+
},
|
|
1856
|
+
);
|
|
1857
|
+
|
|
1858
|
+
expect(result).toHaveLength(1);
|
|
1859
|
+
expect(result[0]).toEqual({ ok: true, newBalance: 150, attempt: 3 });
|
|
1860
|
+
expect(factoryCallCount).toBe(3); // Factory called 3 times (once per attempt)
|
|
1861
|
+
expect(executionAttemptCount).toBe(3); // 3 execution attempts total
|
|
1862
|
+
});
|
|
1863
|
+
});
|
|
1310
1864
|
});
|