@stardeck-customer-apps/testing 0.24.0 → 0.25.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/SKILL.md +4 -1
- package/dist/index.d.mts +31 -1
- package/dist/index.d.ts +31 -1
- package/dist/index.js +105 -1
- package/dist/index.mjs +105 -1
- package/dist/next/headers-shim.js +2 -0
- package/dist/next/headers-shim.mjs +2 -0
- package/dist/setup.js +96 -0
- package/dist/setup.mjs +96 -0
- package/package.json +1 -1
package/SKILL.md
CHANGED
|
@@ -565,7 +565,10 @@ cursor, limit })`; `list()` intentionally keeps its previous array shape.
|
|
|
565
565
|
in `app.storage` against the simulated `STORAGE_URL` host.
|
|
566
566
|
- `client.slack` / `client.line` / `client.facebook` send endpoints — captured
|
|
567
567
|
in `app.messages`.
|
|
568
|
-
- `createEdgeClient()` (print,
|
|
568
|
+
- `createEdgeClient()` (print, `enqueueReceipt()`/`getPrintJob()` durable receipts: idempotent per key,
|
|
569
|
+
conflicts and refused options mirrored, `app.edge.durablePrints`, and
|
|
570
|
+
`app.edge.settleNextDurableJob({ status: "needs_attention", reason, outcome })` to drive the
|
|
571
|
+
uncertain/not-printed UI; pair/unpair, list devices/peripherals, show/clear
|
|
569
572
|
display, test print, `getCapabilities()`) — captured in `app.edge`; seed
|
|
570
573
|
devices/peripherals for pairing pickers via `app.edge.seedDevices()` /
|
|
571
574
|
`seedPeripherals()`.
|
package/dist/index.d.mts
CHANGED
|
@@ -542,6 +542,27 @@ interface CapturedPrint {
|
|
|
542
542
|
logo?: boolean;
|
|
543
543
|
copies: number;
|
|
544
544
|
}
|
|
545
|
+
/** One `edge.enqueueReceipt()` the app made, as the simulator accepted it. */
|
|
546
|
+
interface CapturedDurablePrint {
|
|
547
|
+
jobId: string;
|
|
548
|
+
idempotencyKey: string;
|
|
549
|
+
alias?: string;
|
|
550
|
+
deviceId?: string;
|
|
551
|
+
peripheralId?: string;
|
|
552
|
+
receipt: ReceiptPayload;
|
|
553
|
+
logo?: boolean;
|
|
554
|
+
}
|
|
555
|
+
/** How a simulated durable job settles. Mirrors the platform's status contract. */
|
|
556
|
+
type SimulatedDurableOutcome = {
|
|
557
|
+
status: "sent_to_printer";
|
|
558
|
+
} | {
|
|
559
|
+
status: "queued";
|
|
560
|
+
} | {
|
|
561
|
+
status: "needs_attention";
|
|
562
|
+
reason: string;
|
|
563
|
+
outcome: "not_printed" | "uncertain";
|
|
564
|
+
message?: string;
|
|
565
|
+
};
|
|
545
566
|
interface CapturedDisplay {
|
|
546
567
|
action: "show" | "clear";
|
|
547
568
|
alias?: string;
|
|
@@ -557,6 +578,15 @@ interface TestEdge {
|
|
|
557
578
|
get prints(): CapturedPrint[];
|
|
558
579
|
get displays(): CapturedDisplay[];
|
|
559
580
|
get testPrints(): CapturedTestPrint[];
|
|
581
|
+
/** Durable receipts accepted by `edge.enqueueReceipt()` (one per distinct idempotency key). */
|
|
582
|
+
get durablePrints(): CapturedDurablePrint[];
|
|
583
|
+
/**
|
|
584
|
+
* Decide how the NEXT accepted durable job settles when the app reads it with
|
|
585
|
+
* `edge.getPrintJob()`. Without this every job reads `sent_to_printer`. Queue
|
|
586
|
+
* several to model a run of jobs; use `needs_attention` with `uncertain` to
|
|
587
|
+
* test the "check the printer before reprinting" path.
|
|
588
|
+
*/
|
|
589
|
+
settleNextDurableJob(outcome: SimulatedDurableOutcome): void;
|
|
560
590
|
latestPrint(): CapturedPrint | undefined;
|
|
561
591
|
latestDisplay(): CapturedDisplay | undefined;
|
|
562
592
|
get bindings(): BindingInfo[];
|
|
@@ -742,4 +772,4 @@ declare const TEST_ENV_DEFAULTS: {
|
|
|
742
772
|
/** Default location of the DDL snapshot written by `generate-types`. */
|
|
743
773
|
declare const DEFAULT_SCHEMA_PATH = "./src/generated/data-store-schema.sql";
|
|
744
774
|
|
|
745
|
-
export { type BindingInfo, CONTROL_PLANE_TEST_URL, type CallRouteOptions, type CapturedCheckout, type CapturedDisplay, type CapturedEmail, type CapturedIdentity, type CapturedIdentityLink, type CapturedMessage, type CapturedPrint, type CapturedStarlensCall, type CapturedTestPrint, type CapturedUpload, DATA_STORE_TEST_HOST, DATA_STORE_TEST_URL, DEFAULT_SCHEMA_PATH, type DeviceInfo, type PaymentSimulatorFailure, type PaymentSimulatorOperation, type PeripheralInfo, STORAGE_TEST_HOST, STORAGE_TEST_URL, type SessionTokens, type SimBoltCharge, type SimBoltConnection, type SimBoltIntentRecord, type SimBoltIntentStatus, type StarlensReply, TEST_ENV_DEFAULTS, type TestApp, type TestAppOptions, TestDataStoreInput, type TestDirectory, type TestEdge, type TestInbox, type TestMessages, type TestPayments, type TestStarlens, type TestStorage, type TestUser, WORKFLOW_NAME_PREFIX, callRoute, createTestApp, describeWorkflow, parseWorkflowName };
|
|
775
|
+
export { type BindingInfo, CONTROL_PLANE_TEST_URL, type CallRouteOptions, type CapturedCheckout, type CapturedDisplay, type CapturedDurablePrint, type CapturedEmail, type CapturedIdentity, type CapturedIdentityLink, type CapturedMessage, type CapturedPrint, type CapturedStarlensCall, type CapturedTestPrint, type CapturedUpload, DATA_STORE_TEST_HOST, DATA_STORE_TEST_URL, DEFAULT_SCHEMA_PATH, type DeviceInfo, type PaymentSimulatorFailure, type PaymentSimulatorOperation, type PeripheralInfo, STORAGE_TEST_HOST, STORAGE_TEST_URL, type SessionTokens, type SimBoltCharge, type SimBoltConnection, type SimBoltIntentRecord, type SimBoltIntentStatus, type SimulatedDurableOutcome, type StarlensReply, TEST_ENV_DEFAULTS, type TestApp, type TestAppOptions, TestDataStoreInput, type TestDirectory, type TestEdge, type TestInbox, type TestMessages, type TestPayments, type TestStarlens, type TestStorage, type TestUser, WORKFLOW_NAME_PREFIX, callRoute, createTestApp, describeWorkflow, parseWorkflowName };
|
package/dist/index.d.ts
CHANGED
|
@@ -542,6 +542,27 @@ interface CapturedPrint {
|
|
|
542
542
|
logo?: boolean;
|
|
543
543
|
copies: number;
|
|
544
544
|
}
|
|
545
|
+
/** One `edge.enqueueReceipt()` the app made, as the simulator accepted it. */
|
|
546
|
+
interface CapturedDurablePrint {
|
|
547
|
+
jobId: string;
|
|
548
|
+
idempotencyKey: string;
|
|
549
|
+
alias?: string;
|
|
550
|
+
deviceId?: string;
|
|
551
|
+
peripheralId?: string;
|
|
552
|
+
receipt: ReceiptPayload;
|
|
553
|
+
logo?: boolean;
|
|
554
|
+
}
|
|
555
|
+
/** How a simulated durable job settles. Mirrors the platform's status contract. */
|
|
556
|
+
type SimulatedDurableOutcome = {
|
|
557
|
+
status: "sent_to_printer";
|
|
558
|
+
} | {
|
|
559
|
+
status: "queued";
|
|
560
|
+
} | {
|
|
561
|
+
status: "needs_attention";
|
|
562
|
+
reason: string;
|
|
563
|
+
outcome: "not_printed" | "uncertain";
|
|
564
|
+
message?: string;
|
|
565
|
+
};
|
|
545
566
|
interface CapturedDisplay {
|
|
546
567
|
action: "show" | "clear";
|
|
547
568
|
alias?: string;
|
|
@@ -557,6 +578,15 @@ interface TestEdge {
|
|
|
557
578
|
get prints(): CapturedPrint[];
|
|
558
579
|
get displays(): CapturedDisplay[];
|
|
559
580
|
get testPrints(): CapturedTestPrint[];
|
|
581
|
+
/** Durable receipts accepted by `edge.enqueueReceipt()` (one per distinct idempotency key). */
|
|
582
|
+
get durablePrints(): CapturedDurablePrint[];
|
|
583
|
+
/**
|
|
584
|
+
* Decide how the NEXT accepted durable job settles when the app reads it with
|
|
585
|
+
* `edge.getPrintJob()`. Without this every job reads `sent_to_printer`. Queue
|
|
586
|
+
* several to model a run of jobs; use `needs_attention` with `uncertain` to
|
|
587
|
+
* test the "check the printer before reprinting" path.
|
|
588
|
+
*/
|
|
589
|
+
settleNextDurableJob(outcome: SimulatedDurableOutcome): void;
|
|
560
590
|
latestPrint(): CapturedPrint | undefined;
|
|
561
591
|
latestDisplay(): CapturedDisplay | undefined;
|
|
562
592
|
get bindings(): BindingInfo[];
|
|
@@ -742,4 +772,4 @@ declare const TEST_ENV_DEFAULTS: {
|
|
|
742
772
|
/** Default location of the DDL snapshot written by `generate-types`. */
|
|
743
773
|
declare const DEFAULT_SCHEMA_PATH = "./src/generated/data-store-schema.sql";
|
|
744
774
|
|
|
745
|
-
export { type BindingInfo, CONTROL_PLANE_TEST_URL, type CallRouteOptions, type CapturedCheckout, type CapturedDisplay, type CapturedEmail, type CapturedIdentity, type CapturedIdentityLink, type CapturedMessage, type CapturedPrint, type CapturedStarlensCall, type CapturedTestPrint, type CapturedUpload, DATA_STORE_TEST_HOST, DATA_STORE_TEST_URL, DEFAULT_SCHEMA_PATH, type DeviceInfo, type PaymentSimulatorFailure, type PaymentSimulatorOperation, type PeripheralInfo, STORAGE_TEST_HOST, STORAGE_TEST_URL, type SessionTokens, type SimBoltCharge, type SimBoltConnection, type SimBoltIntentRecord, type SimBoltIntentStatus, type StarlensReply, TEST_ENV_DEFAULTS, type TestApp, type TestAppOptions, TestDataStoreInput, type TestDirectory, type TestEdge, type TestInbox, type TestMessages, type TestPayments, type TestStarlens, type TestStorage, type TestUser, WORKFLOW_NAME_PREFIX, callRoute, createTestApp, describeWorkflow, parseWorkflowName };
|
|
775
|
+
export { type BindingInfo, CONTROL_PLANE_TEST_URL, type CallRouteOptions, type CapturedCheckout, type CapturedDisplay, type CapturedDurablePrint, type CapturedEmail, type CapturedIdentity, type CapturedIdentityLink, type CapturedMessage, type CapturedPrint, type CapturedStarlensCall, type CapturedTestPrint, type CapturedUpload, DATA_STORE_TEST_HOST, DATA_STORE_TEST_URL, DEFAULT_SCHEMA_PATH, type DeviceInfo, type PaymentSimulatorFailure, type PaymentSimulatorOperation, type PeripheralInfo, STORAGE_TEST_HOST, STORAGE_TEST_URL, type SessionTokens, type SimBoltCharge, type SimBoltConnection, type SimBoltIntentRecord, type SimBoltIntentStatus, type SimulatedDurableOutcome, type StarlensReply, TEST_ENV_DEFAULTS, type TestApp, type TestAppOptions, TestDataStoreInput, type TestDirectory, type TestEdge, type TestInbox, type TestMessages, type TestPayments, type TestStarlens, type TestStorage, type TestUser, WORKFLOW_NAME_PREFIX, callRoute, createTestApp, describeWorkflow, parseWorkflowName };
|
package/dist/index.js
CHANGED
|
@@ -365,6 +365,8 @@ var state = globalSingleton("state", () => ({
|
|
|
365
365
|
edgeDisplays: [],
|
|
366
366
|
edgeTestPrints: [],
|
|
367
367
|
edgePrintCounter: 0,
|
|
368
|
+
edgeDurableJobs: /* @__PURE__ */ new Map(),
|
|
369
|
+
edgeDurableOutcomes: [],
|
|
368
370
|
edgeDevices: [],
|
|
369
371
|
edgePeripherals: [],
|
|
370
372
|
edgeBindings: /* @__PURE__ */ new Map(),
|
|
@@ -3382,6 +3384,93 @@ async function handlePrint(request) {
|
|
|
3382
3384
|
status: "completed"
|
|
3383
3385
|
});
|
|
3384
3386
|
}
|
|
3387
|
+
var IDEMPOTENCY_KEY = /^[A-Za-z0-9_.:-]{1,128}$/;
|
|
3388
|
+
async function handleEnqueue(request) {
|
|
3389
|
+
const body = await readJsonBody(request);
|
|
3390
|
+
const key = typeof body.idempotencyKey === "string" ? body.idempotencyKey : "";
|
|
3391
|
+
if (!IDEMPOTENCY_KEY.test(key)) {
|
|
3392
|
+
return edgeFailure("idempotencyKey must be 1-128 characters of A-Z a-z 0-9 _ . : -");
|
|
3393
|
+
}
|
|
3394
|
+
if (body.openDrawer === true) {
|
|
3395
|
+
return edgeFailure("Durable receipts cannot open the cash drawer; a retry would kick it again");
|
|
3396
|
+
}
|
|
3397
|
+
if (body.copies !== void 0 && body.copies !== 1) {
|
|
3398
|
+
return edgeFailure(
|
|
3399
|
+
"Durable receipts print exactly one copy; submit a second job for another copy"
|
|
3400
|
+
);
|
|
3401
|
+
}
|
|
3402
|
+
const receipt = body.receipt ?? {};
|
|
3403
|
+
if (receipt.rawEscPos !== void 0) {
|
|
3404
|
+
return edgeFailure("rawEscPos is not supported for durable receipts");
|
|
3405
|
+
}
|
|
3406
|
+
const requestHash = JSON.stringify([
|
|
3407
|
+
body.alias,
|
|
3408
|
+
body.deviceId,
|
|
3409
|
+
body.peripheralId,
|
|
3410
|
+
receipt,
|
|
3411
|
+
body.logo
|
|
3412
|
+
]);
|
|
3413
|
+
const existing = [...state.edgeDurableJobs.values()].find(
|
|
3414
|
+
(j) => j.captured.idempotencyKey === key
|
|
3415
|
+
);
|
|
3416
|
+
if (existing) {
|
|
3417
|
+
if (existing.requestHash !== requestHash) {
|
|
3418
|
+
return edgeFailure(
|
|
3419
|
+
"This idempotencyKey was already used for a different receipt, target or options.",
|
|
3420
|
+
409,
|
|
3421
|
+
"IDEMPOTENCY_KEY_CONFLICT"
|
|
3422
|
+
);
|
|
3423
|
+
}
|
|
3424
|
+
return json({ success: true, data: durableView(existing.captured.jobId, "queued") }, 200);
|
|
3425
|
+
}
|
|
3426
|
+
const jobId = nextJobId();
|
|
3427
|
+
const captured = {
|
|
3428
|
+
jobId,
|
|
3429
|
+
idempotencyKey: key,
|
|
3430
|
+
alias: body.alias ? String(body.alias) : void 0,
|
|
3431
|
+
deviceId: body.deviceId ? String(body.deviceId) : void 0,
|
|
3432
|
+
peripheralId: body.peripheralId ? String(body.peripheralId) : void 0,
|
|
3433
|
+
receipt,
|
|
3434
|
+
logo: body.logo === true ? true : body.logo === false ? false : void 0
|
|
3435
|
+
};
|
|
3436
|
+
const outcome = state.edgeDurableOutcomes.shift() ?? { status: "sent_to_printer" };
|
|
3437
|
+
state.edgeDurableJobs.set(jobId, { requestHash, captured, outcome });
|
|
3438
|
+
return json({ success: true, data: durableView(jobId, "queued") }, 202);
|
|
3439
|
+
}
|
|
3440
|
+
function durableView(jobId, as) {
|
|
3441
|
+
const job = state.edgeDurableJobs.get(jobId);
|
|
3442
|
+
const createdAt = now2();
|
|
3443
|
+
const base = {
|
|
3444
|
+
jobId,
|
|
3445
|
+
attempts: as === "queued" ? 0 : 1,
|
|
3446
|
+
createdAt,
|
|
3447
|
+
expiresAt: new Date(Date.parse(createdAt) + 10 * 6e4).toISOString(),
|
|
3448
|
+
target: {
|
|
3449
|
+
deviceId: job.captured.deviceId ?? "sim-device",
|
|
3450
|
+
peripheralId: job.captured.peripheralId ?? "sim-peripheral"
|
|
3451
|
+
}
|
|
3452
|
+
};
|
|
3453
|
+
const outcome = as === "queued" ? { status: "queued" } : job.outcome;
|
|
3454
|
+
if (outcome.status === "needs_attention") {
|
|
3455
|
+
return {
|
|
3456
|
+
...base,
|
|
3457
|
+
status: "needs_attention",
|
|
3458
|
+
attention: {
|
|
3459
|
+
reason: outcome.reason,
|
|
3460
|
+
outcome: outcome.outcome,
|
|
3461
|
+
message: outcome.message ?? "Delivery stopped."
|
|
3462
|
+
}
|
|
3463
|
+
};
|
|
3464
|
+
}
|
|
3465
|
+
if (outcome.status === "sent_to_printer") {
|
|
3466
|
+
return { ...base, status: "sent_to_printer", sentToPrinterAt: createdAt };
|
|
3467
|
+
}
|
|
3468
|
+
return { ...base, status: "queued" };
|
|
3469
|
+
}
|
|
3470
|
+
function handleGetDurable(jobId) {
|
|
3471
|
+
if (!state.edgeDurableJobs.has(jobId)) return edgeFailure("Print job not found", 404);
|
|
3472
|
+
return success(durableView(jobId));
|
|
3473
|
+
}
|
|
3385
3474
|
async function handleShowDisplay(request) {
|
|
3386
3475
|
const body = await readJsonBody(request);
|
|
3387
3476
|
const captured = {
|
|
@@ -3431,6 +3520,13 @@ async function handleEdgeRequest(request, subPath) {
|
|
|
3431
3520
|
if (subPath === "/print" && method === "POST") {
|
|
3432
3521
|
return handlePrint(request);
|
|
3433
3522
|
}
|
|
3523
|
+
if (subPath === "/print-jobs" && method === "POST") {
|
|
3524
|
+
return handleEnqueue(request);
|
|
3525
|
+
}
|
|
3526
|
+
const durableMatch = subPath.match(/^\/print-jobs\/([^/]+)$/);
|
|
3527
|
+
if (durableMatch && method === "GET") {
|
|
3528
|
+
return handleGetDurable(decodeURIComponent(durableMatch[1]));
|
|
3529
|
+
}
|
|
3434
3530
|
if (subPath === "/capabilities" && method === "GET") {
|
|
3435
3531
|
return handleCapabilities(request);
|
|
3436
3532
|
}
|
|
@@ -3474,6 +3570,12 @@ function createEdge() {
|
|
|
3474
3570
|
get testPrints() {
|
|
3475
3571
|
return [...state.edgeTestPrints];
|
|
3476
3572
|
},
|
|
3573
|
+
get durablePrints() {
|
|
3574
|
+
return [...state.edgeDurableJobs.values()].map((j) => j.captured);
|
|
3575
|
+
},
|
|
3576
|
+
settleNextDurableJob(outcome) {
|
|
3577
|
+
state.edgeDurableOutcomes.push(outcome);
|
|
3578
|
+
},
|
|
3477
3579
|
latestPrint() {
|
|
3478
3580
|
return state.edgePrints[state.edgePrints.length - 1];
|
|
3479
3581
|
},
|
|
@@ -3510,12 +3612,14 @@ function createEdge() {
|
|
|
3510
3612
|
state.edgeDisplays = [];
|
|
3511
3613
|
state.edgeTestPrints = [];
|
|
3512
3614
|
state.edgePrintCounter = 0;
|
|
3615
|
+
state.edgeDurableJobs.clear();
|
|
3616
|
+
state.edgeDurableOutcomes = [];
|
|
3513
3617
|
state.edgeDevices = [];
|
|
3514
3618
|
state.edgePeripherals = [];
|
|
3515
3619
|
state.edgeBindings.clear();
|
|
3516
3620
|
},
|
|
3517
3621
|
get count() {
|
|
3518
|
-
return state.edgePrints.length + state.edgeDisplays.length + state.edgeTestPrints.length;
|
|
3622
|
+
return state.edgePrints.length + state.edgeDisplays.length + state.edgeTestPrints.length + state.edgeDurableJobs.size;
|
|
3519
3623
|
}
|
|
3520
3624
|
};
|
|
3521
3625
|
}
|
package/dist/index.mjs
CHANGED
|
@@ -326,6 +326,8 @@ var state = globalSingleton("state", () => ({
|
|
|
326
326
|
edgeDisplays: [],
|
|
327
327
|
edgeTestPrints: [],
|
|
328
328
|
edgePrintCounter: 0,
|
|
329
|
+
edgeDurableJobs: /* @__PURE__ */ new Map(),
|
|
330
|
+
edgeDurableOutcomes: [],
|
|
329
331
|
edgeDevices: [],
|
|
330
332
|
edgePeripherals: [],
|
|
331
333
|
edgeBindings: /* @__PURE__ */ new Map(),
|
|
@@ -3343,6 +3345,93 @@ async function handlePrint(request) {
|
|
|
3343
3345
|
status: "completed"
|
|
3344
3346
|
});
|
|
3345
3347
|
}
|
|
3348
|
+
var IDEMPOTENCY_KEY = /^[A-Za-z0-9_.:-]{1,128}$/;
|
|
3349
|
+
async function handleEnqueue(request) {
|
|
3350
|
+
const body = await readJsonBody(request);
|
|
3351
|
+
const key = typeof body.idempotencyKey === "string" ? body.idempotencyKey : "";
|
|
3352
|
+
if (!IDEMPOTENCY_KEY.test(key)) {
|
|
3353
|
+
return edgeFailure("idempotencyKey must be 1-128 characters of A-Z a-z 0-9 _ . : -");
|
|
3354
|
+
}
|
|
3355
|
+
if (body.openDrawer === true) {
|
|
3356
|
+
return edgeFailure("Durable receipts cannot open the cash drawer; a retry would kick it again");
|
|
3357
|
+
}
|
|
3358
|
+
if (body.copies !== void 0 && body.copies !== 1) {
|
|
3359
|
+
return edgeFailure(
|
|
3360
|
+
"Durable receipts print exactly one copy; submit a second job for another copy"
|
|
3361
|
+
);
|
|
3362
|
+
}
|
|
3363
|
+
const receipt = body.receipt ?? {};
|
|
3364
|
+
if (receipt.rawEscPos !== void 0) {
|
|
3365
|
+
return edgeFailure("rawEscPos is not supported for durable receipts");
|
|
3366
|
+
}
|
|
3367
|
+
const requestHash = JSON.stringify([
|
|
3368
|
+
body.alias,
|
|
3369
|
+
body.deviceId,
|
|
3370
|
+
body.peripheralId,
|
|
3371
|
+
receipt,
|
|
3372
|
+
body.logo
|
|
3373
|
+
]);
|
|
3374
|
+
const existing = [...state.edgeDurableJobs.values()].find(
|
|
3375
|
+
(j) => j.captured.idempotencyKey === key
|
|
3376
|
+
);
|
|
3377
|
+
if (existing) {
|
|
3378
|
+
if (existing.requestHash !== requestHash) {
|
|
3379
|
+
return edgeFailure(
|
|
3380
|
+
"This idempotencyKey was already used for a different receipt, target or options.",
|
|
3381
|
+
409,
|
|
3382
|
+
"IDEMPOTENCY_KEY_CONFLICT"
|
|
3383
|
+
);
|
|
3384
|
+
}
|
|
3385
|
+
return json({ success: true, data: durableView(existing.captured.jobId, "queued") }, 200);
|
|
3386
|
+
}
|
|
3387
|
+
const jobId = nextJobId();
|
|
3388
|
+
const captured = {
|
|
3389
|
+
jobId,
|
|
3390
|
+
idempotencyKey: key,
|
|
3391
|
+
alias: body.alias ? String(body.alias) : void 0,
|
|
3392
|
+
deviceId: body.deviceId ? String(body.deviceId) : void 0,
|
|
3393
|
+
peripheralId: body.peripheralId ? String(body.peripheralId) : void 0,
|
|
3394
|
+
receipt,
|
|
3395
|
+
logo: body.logo === true ? true : body.logo === false ? false : void 0
|
|
3396
|
+
};
|
|
3397
|
+
const outcome = state.edgeDurableOutcomes.shift() ?? { status: "sent_to_printer" };
|
|
3398
|
+
state.edgeDurableJobs.set(jobId, { requestHash, captured, outcome });
|
|
3399
|
+
return json({ success: true, data: durableView(jobId, "queued") }, 202);
|
|
3400
|
+
}
|
|
3401
|
+
function durableView(jobId, as) {
|
|
3402
|
+
const job = state.edgeDurableJobs.get(jobId);
|
|
3403
|
+
const createdAt = now2();
|
|
3404
|
+
const base = {
|
|
3405
|
+
jobId,
|
|
3406
|
+
attempts: as === "queued" ? 0 : 1,
|
|
3407
|
+
createdAt,
|
|
3408
|
+
expiresAt: new Date(Date.parse(createdAt) + 10 * 6e4).toISOString(),
|
|
3409
|
+
target: {
|
|
3410
|
+
deviceId: job.captured.deviceId ?? "sim-device",
|
|
3411
|
+
peripheralId: job.captured.peripheralId ?? "sim-peripheral"
|
|
3412
|
+
}
|
|
3413
|
+
};
|
|
3414
|
+
const outcome = as === "queued" ? { status: "queued" } : job.outcome;
|
|
3415
|
+
if (outcome.status === "needs_attention") {
|
|
3416
|
+
return {
|
|
3417
|
+
...base,
|
|
3418
|
+
status: "needs_attention",
|
|
3419
|
+
attention: {
|
|
3420
|
+
reason: outcome.reason,
|
|
3421
|
+
outcome: outcome.outcome,
|
|
3422
|
+
message: outcome.message ?? "Delivery stopped."
|
|
3423
|
+
}
|
|
3424
|
+
};
|
|
3425
|
+
}
|
|
3426
|
+
if (outcome.status === "sent_to_printer") {
|
|
3427
|
+
return { ...base, status: "sent_to_printer", sentToPrinterAt: createdAt };
|
|
3428
|
+
}
|
|
3429
|
+
return { ...base, status: "queued" };
|
|
3430
|
+
}
|
|
3431
|
+
function handleGetDurable(jobId) {
|
|
3432
|
+
if (!state.edgeDurableJobs.has(jobId)) return edgeFailure("Print job not found", 404);
|
|
3433
|
+
return success(durableView(jobId));
|
|
3434
|
+
}
|
|
3346
3435
|
async function handleShowDisplay(request) {
|
|
3347
3436
|
const body = await readJsonBody(request);
|
|
3348
3437
|
const captured = {
|
|
@@ -3392,6 +3481,13 @@ async function handleEdgeRequest(request, subPath) {
|
|
|
3392
3481
|
if (subPath === "/print" && method === "POST") {
|
|
3393
3482
|
return handlePrint(request);
|
|
3394
3483
|
}
|
|
3484
|
+
if (subPath === "/print-jobs" && method === "POST") {
|
|
3485
|
+
return handleEnqueue(request);
|
|
3486
|
+
}
|
|
3487
|
+
const durableMatch = subPath.match(/^\/print-jobs\/([^/]+)$/);
|
|
3488
|
+
if (durableMatch && method === "GET") {
|
|
3489
|
+
return handleGetDurable(decodeURIComponent(durableMatch[1]));
|
|
3490
|
+
}
|
|
3395
3491
|
if (subPath === "/capabilities" && method === "GET") {
|
|
3396
3492
|
return handleCapabilities(request);
|
|
3397
3493
|
}
|
|
@@ -3435,6 +3531,12 @@ function createEdge() {
|
|
|
3435
3531
|
get testPrints() {
|
|
3436
3532
|
return [...state.edgeTestPrints];
|
|
3437
3533
|
},
|
|
3534
|
+
get durablePrints() {
|
|
3535
|
+
return [...state.edgeDurableJobs.values()].map((j) => j.captured);
|
|
3536
|
+
},
|
|
3537
|
+
settleNextDurableJob(outcome) {
|
|
3538
|
+
state.edgeDurableOutcomes.push(outcome);
|
|
3539
|
+
},
|
|
3438
3540
|
latestPrint() {
|
|
3439
3541
|
return state.edgePrints[state.edgePrints.length - 1];
|
|
3440
3542
|
},
|
|
@@ -3471,12 +3573,14 @@ function createEdge() {
|
|
|
3471
3573
|
state.edgeDisplays = [];
|
|
3472
3574
|
state.edgeTestPrints = [];
|
|
3473
3575
|
state.edgePrintCounter = 0;
|
|
3576
|
+
state.edgeDurableJobs.clear();
|
|
3577
|
+
state.edgeDurableOutcomes = [];
|
|
3474
3578
|
state.edgeDevices = [];
|
|
3475
3579
|
state.edgePeripherals = [];
|
|
3476
3580
|
state.edgeBindings.clear();
|
|
3477
3581
|
},
|
|
3478
3582
|
get count() {
|
|
3479
|
-
return state.edgePrints.length + state.edgeDisplays.length + state.edgeTestPrints.length;
|
|
3583
|
+
return state.edgePrints.length + state.edgeDisplays.length + state.edgeTestPrints.length + state.edgeDurableJobs.size;
|
|
3480
3584
|
}
|
|
3481
3585
|
};
|
|
3482
3586
|
}
|
|
@@ -87,6 +87,8 @@ var state = globalSingleton("state", () => ({
|
|
|
87
87
|
edgeDisplays: [],
|
|
88
88
|
edgeTestPrints: [],
|
|
89
89
|
edgePrintCounter: 0,
|
|
90
|
+
edgeDurableJobs: /* @__PURE__ */ new Map(),
|
|
91
|
+
edgeDurableOutcomes: [],
|
|
90
92
|
edgeDevices: [],
|
|
91
93
|
edgePeripherals: [],
|
|
92
94
|
edgeBindings: /* @__PURE__ */ new Map(),
|
|
@@ -60,6 +60,8 @@ var state = globalSingleton("state", () => ({
|
|
|
60
60
|
edgeDisplays: [],
|
|
61
61
|
edgeTestPrints: [],
|
|
62
62
|
edgePrintCounter: 0,
|
|
63
|
+
edgeDurableJobs: /* @__PURE__ */ new Map(),
|
|
64
|
+
edgeDurableOutcomes: [],
|
|
63
65
|
edgeDevices: [],
|
|
64
66
|
edgePeripherals: [],
|
|
65
67
|
edgeBindings: /* @__PURE__ */ new Map(),
|
package/dist/setup.js
CHANGED
|
@@ -92,6 +92,8 @@ var state = globalSingleton("state", () => ({
|
|
|
92
92
|
edgeDisplays: [],
|
|
93
93
|
edgeTestPrints: [],
|
|
94
94
|
edgePrintCounter: 0,
|
|
95
|
+
edgeDurableJobs: /* @__PURE__ */ new Map(),
|
|
96
|
+
edgeDurableOutcomes: [],
|
|
95
97
|
edgeDevices: [],
|
|
96
98
|
edgePeripherals: [],
|
|
97
99
|
edgeBindings: /* @__PURE__ */ new Map(),
|
|
@@ -2527,6 +2529,93 @@ async function handlePrint(request) {
|
|
|
2527
2529
|
status: "completed"
|
|
2528
2530
|
});
|
|
2529
2531
|
}
|
|
2532
|
+
var IDEMPOTENCY_KEY = /^[A-Za-z0-9_.:-]{1,128}$/;
|
|
2533
|
+
async function handleEnqueue(request) {
|
|
2534
|
+
const body = await readJsonBody(request);
|
|
2535
|
+
const key = typeof body.idempotencyKey === "string" ? body.idempotencyKey : "";
|
|
2536
|
+
if (!IDEMPOTENCY_KEY.test(key)) {
|
|
2537
|
+
return edgeFailure("idempotencyKey must be 1-128 characters of A-Z a-z 0-9 _ . : -");
|
|
2538
|
+
}
|
|
2539
|
+
if (body.openDrawer === true) {
|
|
2540
|
+
return edgeFailure("Durable receipts cannot open the cash drawer; a retry would kick it again");
|
|
2541
|
+
}
|
|
2542
|
+
if (body.copies !== void 0 && body.copies !== 1) {
|
|
2543
|
+
return edgeFailure(
|
|
2544
|
+
"Durable receipts print exactly one copy; submit a second job for another copy"
|
|
2545
|
+
);
|
|
2546
|
+
}
|
|
2547
|
+
const receipt = body.receipt ?? {};
|
|
2548
|
+
if (receipt.rawEscPos !== void 0) {
|
|
2549
|
+
return edgeFailure("rawEscPos is not supported for durable receipts");
|
|
2550
|
+
}
|
|
2551
|
+
const requestHash = JSON.stringify([
|
|
2552
|
+
body.alias,
|
|
2553
|
+
body.deviceId,
|
|
2554
|
+
body.peripheralId,
|
|
2555
|
+
receipt,
|
|
2556
|
+
body.logo
|
|
2557
|
+
]);
|
|
2558
|
+
const existing = [...state.edgeDurableJobs.values()].find(
|
|
2559
|
+
(j) => j.captured.idempotencyKey === key
|
|
2560
|
+
);
|
|
2561
|
+
if (existing) {
|
|
2562
|
+
if (existing.requestHash !== requestHash) {
|
|
2563
|
+
return edgeFailure(
|
|
2564
|
+
"This idempotencyKey was already used for a different receipt, target or options.",
|
|
2565
|
+
409,
|
|
2566
|
+
"IDEMPOTENCY_KEY_CONFLICT"
|
|
2567
|
+
);
|
|
2568
|
+
}
|
|
2569
|
+
return json({ success: true, data: durableView(existing.captured.jobId, "queued") }, 200);
|
|
2570
|
+
}
|
|
2571
|
+
const jobId = nextJobId();
|
|
2572
|
+
const captured = {
|
|
2573
|
+
jobId,
|
|
2574
|
+
idempotencyKey: key,
|
|
2575
|
+
alias: body.alias ? String(body.alias) : void 0,
|
|
2576
|
+
deviceId: body.deviceId ? String(body.deviceId) : void 0,
|
|
2577
|
+
peripheralId: body.peripheralId ? String(body.peripheralId) : void 0,
|
|
2578
|
+
receipt,
|
|
2579
|
+
logo: body.logo === true ? true : body.logo === false ? false : void 0
|
|
2580
|
+
};
|
|
2581
|
+
const outcome = state.edgeDurableOutcomes.shift() ?? { status: "sent_to_printer" };
|
|
2582
|
+
state.edgeDurableJobs.set(jobId, { requestHash, captured, outcome });
|
|
2583
|
+
return json({ success: true, data: durableView(jobId, "queued") }, 202);
|
|
2584
|
+
}
|
|
2585
|
+
function durableView(jobId, as) {
|
|
2586
|
+
const job = state.edgeDurableJobs.get(jobId);
|
|
2587
|
+
const createdAt = now2();
|
|
2588
|
+
const base = {
|
|
2589
|
+
jobId,
|
|
2590
|
+
attempts: as === "queued" ? 0 : 1,
|
|
2591
|
+
createdAt,
|
|
2592
|
+
expiresAt: new Date(Date.parse(createdAt) + 10 * 6e4).toISOString(),
|
|
2593
|
+
target: {
|
|
2594
|
+
deviceId: job.captured.deviceId ?? "sim-device",
|
|
2595
|
+
peripheralId: job.captured.peripheralId ?? "sim-peripheral"
|
|
2596
|
+
}
|
|
2597
|
+
};
|
|
2598
|
+
const outcome = as === "queued" ? { status: "queued" } : job.outcome;
|
|
2599
|
+
if (outcome.status === "needs_attention") {
|
|
2600
|
+
return {
|
|
2601
|
+
...base,
|
|
2602
|
+
status: "needs_attention",
|
|
2603
|
+
attention: {
|
|
2604
|
+
reason: outcome.reason,
|
|
2605
|
+
outcome: outcome.outcome,
|
|
2606
|
+
message: outcome.message ?? "Delivery stopped."
|
|
2607
|
+
}
|
|
2608
|
+
};
|
|
2609
|
+
}
|
|
2610
|
+
if (outcome.status === "sent_to_printer") {
|
|
2611
|
+
return { ...base, status: "sent_to_printer", sentToPrinterAt: createdAt };
|
|
2612
|
+
}
|
|
2613
|
+
return { ...base, status: "queued" };
|
|
2614
|
+
}
|
|
2615
|
+
function handleGetDurable(jobId) {
|
|
2616
|
+
if (!state.edgeDurableJobs.has(jobId)) return edgeFailure("Print job not found", 404);
|
|
2617
|
+
return success(durableView(jobId));
|
|
2618
|
+
}
|
|
2530
2619
|
async function handleShowDisplay(request) {
|
|
2531
2620
|
const body = await readJsonBody(request);
|
|
2532
2621
|
const captured = {
|
|
@@ -2576,6 +2665,13 @@ async function handleEdgeRequest(request, subPath) {
|
|
|
2576
2665
|
if (subPath === "/print" && method === "POST") {
|
|
2577
2666
|
return handlePrint(request);
|
|
2578
2667
|
}
|
|
2668
|
+
if (subPath === "/print-jobs" && method === "POST") {
|
|
2669
|
+
return handleEnqueue(request);
|
|
2670
|
+
}
|
|
2671
|
+
const durableMatch = subPath.match(/^\/print-jobs\/([^/]+)$/);
|
|
2672
|
+
if (durableMatch && method === "GET") {
|
|
2673
|
+
return handleGetDurable(decodeURIComponent(durableMatch[1]));
|
|
2674
|
+
}
|
|
2579
2675
|
if (subPath === "/capabilities" && method === "GET") {
|
|
2580
2676
|
return handleCapabilities(request);
|
|
2581
2677
|
}
|
package/dist/setup.mjs
CHANGED
|
@@ -68,6 +68,8 @@ var state = globalSingleton("state", () => ({
|
|
|
68
68
|
edgeDisplays: [],
|
|
69
69
|
edgeTestPrints: [],
|
|
70
70
|
edgePrintCounter: 0,
|
|
71
|
+
edgeDurableJobs: /* @__PURE__ */ new Map(),
|
|
72
|
+
edgeDurableOutcomes: [],
|
|
71
73
|
edgeDevices: [],
|
|
72
74
|
edgePeripherals: [],
|
|
73
75
|
edgeBindings: /* @__PURE__ */ new Map(),
|
|
@@ -2503,6 +2505,93 @@ async function handlePrint(request) {
|
|
|
2503
2505
|
status: "completed"
|
|
2504
2506
|
});
|
|
2505
2507
|
}
|
|
2508
|
+
var IDEMPOTENCY_KEY = /^[A-Za-z0-9_.:-]{1,128}$/;
|
|
2509
|
+
async function handleEnqueue(request) {
|
|
2510
|
+
const body = await readJsonBody(request);
|
|
2511
|
+
const key = typeof body.idempotencyKey === "string" ? body.idempotencyKey : "";
|
|
2512
|
+
if (!IDEMPOTENCY_KEY.test(key)) {
|
|
2513
|
+
return edgeFailure("idempotencyKey must be 1-128 characters of A-Z a-z 0-9 _ . : -");
|
|
2514
|
+
}
|
|
2515
|
+
if (body.openDrawer === true) {
|
|
2516
|
+
return edgeFailure("Durable receipts cannot open the cash drawer; a retry would kick it again");
|
|
2517
|
+
}
|
|
2518
|
+
if (body.copies !== void 0 && body.copies !== 1) {
|
|
2519
|
+
return edgeFailure(
|
|
2520
|
+
"Durable receipts print exactly one copy; submit a second job for another copy"
|
|
2521
|
+
);
|
|
2522
|
+
}
|
|
2523
|
+
const receipt = body.receipt ?? {};
|
|
2524
|
+
if (receipt.rawEscPos !== void 0) {
|
|
2525
|
+
return edgeFailure("rawEscPos is not supported for durable receipts");
|
|
2526
|
+
}
|
|
2527
|
+
const requestHash = JSON.stringify([
|
|
2528
|
+
body.alias,
|
|
2529
|
+
body.deviceId,
|
|
2530
|
+
body.peripheralId,
|
|
2531
|
+
receipt,
|
|
2532
|
+
body.logo
|
|
2533
|
+
]);
|
|
2534
|
+
const existing = [...state.edgeDurableJobs.values()].find(
|
|
2535
|
+
(j) => j.captured.idempotencyKey === key
|
|
2536
|
+
);
|
|
2537
|
+
if (existing) {
|
|
2538
|
+
if (existing.requestHash !== requestHash) {
|
|
2539
|
+
return edgeFailure(
|
|
2540
|
+
"This idempotencyKey was already used for a different receipt, target or options.",
|
|
2541
|
+
409,
|
|
2542
|
+
"IDEMPOTENCY_KEY_CONFLICT"
|
|
2543
|
+
);
|
|
2544
|
+
}
|
|
2545
|
+
return json({ success: true, data: durableView(existing.captured.jobId, "queued") }, 200);
|
|
2546
|
+
}
|
|
2547
|
+
const jobId = nextJobId();
|
|
2548
|
+
const captured = {
|
|
2549
|
+
jobId,
|
|
2550
|
+
idempotencyKey: key,
|
|
2551
|
+
alias: body.alias ? String(body.alias) : void 0,
|
|
2552
|
+
deviceId: body.deviceId ? String(body.deviceId) : void 0,
|
|
2553
|
+
peripheralId: body.peripheralId ? String(body.peripheralId) : void 0,
|
|
2554
|
+
receipt,
|
|
2555
|
+
logo: body.logo === true ? true : body.logo === false ? false : void 0
|
|
2556
|
+
};
|
|
2557
|
+
const outcome = state.edgeDurableOutcomes.shift() ?? { status: "sent_to_printer" };
|
|
2558
|
+
state.edgeDurableJobs.set(jobId, { requestHash, captured, outcome });
|
|
2559
|
+
return json({ success: true, data: durableView(jobId, "queued") }, 202);
|
|
2560
|
+
}
|
|
2561
|
+
function durableView(jobId, as) {
|
|
2562
|
+
const job = state.edgeDurableJobs.get(jobId);
|
|
2563
|
+
const createdAt = now2();
|
|
2564
|
+
const base = {
|
|
2565
|
+
jobId,
|
|
2566
|
+
attempts: as === "queued" ? 0 : 1,
|
|
2567
|
+
createdAt,
|
|
2568
|
+
expiresAt: new Date(Date.parse(createdAt) + 10 * 6e4).toISOString(),
|
|
2569
|
+
target: {
|
|
2570
|
+
deviceId: job.captured.deviceId ?? "sim-device",
|
|
2571
|
+
peripheralId: job.captured.peripheralId ?? "sim-peripheral"
|
|
2572
|
+
}
|
|
2573
|
+
};
|
|
2574
|
+
const outcome = as === "queued" ? { status: "queued" } : job.outcome;
|
|
2575
|
+
if (outcome.status === "needs_attention") {
|
|
2576
|
+
return {
|
|
2577
|
+
...base,
|
|
2578
|
+
status: "needs_attention",
|
|
2579
|
+
attention: {
|
|
2580
|
+
reason: outcome.reason,
|
|
2581
|
+
outcome: outcome.outcome,
|
|
2582
|
+
message: outcome.message ?? "Delivery stopped."
|
|
2583
|
+
}
|
|
2584
|
+
};
|
|
2585
|
+
}
|
|
2586
|
+
if (outcome.status === "sent_to_printer") {
|
|
2587
|
+
return { ...base, status: "sent_to_printer", sentToPrinterAt: createdAt };
|
|
2588
|
+
}
|
|
2589
|
+
return { ...base, status: "queued" };
|
|
2590
|
+
}
|
|
2591
|
+
function handleGetDurable(jobId) {
|
|
2592
|
+
if (!state.edgeDurableJobs.has(jobId)) return edgeFailure("Print job not found", 404);
|
|
2593
|
+
return success(durableView(jobId));
|
|
2594
|
+
}
|
|
2506
2595
|
async function handleShowDisplay(request) {
|
|
2507
2596
|
const body = await readJsonBody(request);
|
|
2508
2597
|
const captured = {
|
|
@@ -2552,6 +2641,13 @@ async function handleEdgeRequest(request, subPath) {
|
|
|
2552
2641
|
if (subPath === "/print" && method === "POST") {
|
|
2553
2642
|
return handlePrint(request);
|
|
2554
2643
|
}
|
|
2644
|
+
if (subPath === "/print-jobs" && method === "POST") {
|
|
2645
|
+
return handleEnqueue(request);
|
|
2646
|
+
}
|
|
2647
|
+
const durableMatch = subPath.match(/^\/print-jobs\/([^/]+)$/);
|
|
2648
|
+
if (durableMatch && method === "GET") {
|
|
2649
|
+
return handleGetDurable(decodeURIComponent(durableMatch[1]));
|
|
2650
|
+
}
|
|
2555
2651
|
if (subPath === "/capabilities" && method === "GET") {
|
|
2556
2652
|
return handleCapabilities(request);
|
|
2557
2653
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stardeck-customer-apps/testing",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.25.0",
|
|
4
4
|
"description": "Vitest test harness for Stardeck customer apps — in-process Postgres (PGlite) plus a control-plane simulator so the real Stardeck SDKs run unmodified in tests",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|