@aplaytest/store-azure 0.1.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/dist/.tsbuildinfo +1 -0
- package/dist/azure-backend.d.ts +59 -0
- package/dist/azure-backend.d.ts.map +1 -0
- package/dist/azure-backend.js +95 -0
- package/dist/azure-backend.js.map +1 -0
- package/dist/backend.d.ts +30 -0
- package/dist/backend.d.ts.map +1 -0
- package/dist/backend.js +29 -0
- package/dist/backend.js.map +1 -0
- package/dist/blob-store.d.ts +159 -0
- package/dist/blob-store.d.ts.map +1 -0
- package/dist/blob-store.js +282 -0
- package/dist/blob-store.js.map +1 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -0
- package/dist/layout.d.ts +55 -0
- package/dist/layout.d.ts.map +1 -0
- package/dist/layout.js +115 -0
- package/dist/layout.js.map +1 -0
- package/package.json +40 -0
package/dist/layout.js
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The blob naming scheme — the whole concurrency story, encoded in a path.
|
|
3
|
+
*
|
|
4
|
+
* The store this replaces was one SQLite file on a blob: download it, ingest,
|
|
5
|
+
* upload it back under an ETag precondition. That works exactly until two runs
|
|
6
|
+
* overlap, at which point the loser either fails the step or discards the
|
|
7
|
+
* winner's attempts, and it re-uploads the entire history to record thirty
|
|
8
|
+
* seconds of it.
|
|
9
|
+
*
|
|
10
|
+
* Here a run record is an object and the container is an append-only log:
|
|
11
|
+
*
|
|
12
|
+
* <prefix>v1/runs/2026/08/30/<runId>/<shard>.json.gz
|
|
13
|
+
*
|
|
14
|
+
* Three properties follow directly from the name, with no locking:
|
|
15
|
+
*
|
|
16
|
+
* · IDEMPOTENT. A re-ingested shard computes the same name and overwrites
|
|
17
|
+
* itself. The scoped-delete and UPSERT gymnastics the SQL driver needs to
|
|
18
|
+
* avoid double-counting are a naming rule here.
|
|
19
|
+
* · CONCURRENT. Two shards of one run, and two overlapping main-branch runs,
|
|
20
|
+
* write different names. Nothing races, so nothing needs a precondition.
|
|
21
|
+
* · CHEAP TO WINDOW. The date is in the path, so a 90-day read filters the
|
|
22
|
+
* LISTING and downloads only what it will score, and the run id is in the
|
|
23
|
+
* path, so counting runs needs no downloads at all.
|
|
24
|
+
*
|
|
25
|
+
* All pure string work: no client, no credential, no network.
|
|
26
|
+
*/
|
|
27
|
+
/** Bumped only for a change no existing reader can parse. Old prefixes stay readable. */
|
|
28
|
+
export const LAYOUT_VERSION = 'v1';
|
|
29
|
+
const RUNS_ROOT = `${LAYOUT_VERSION}/runs`;
|
|
30
|
+
const SUFFIX = '.json.gz';
|
|
31
|
+
/**
|
|
32
|
+
* Run ids come from `ATEST_RUN_ID`, which in CI is whatever the pipeline puts
|
|
33
|
+
* there — `github.run_id`, a pipeline id, a branch name. A `/` in one would
|
|
34
|
+
* silently add a directory level and break parsing; a space or a `#` would
|
|
35
|
+
* survive one SDK and not the next. Encode to a conservative alphabet and keep
|
|
36
|
+
* it reversible, so the run id can still be read back out of a listing.
|
|
37
|
+
*/
|
|
38
|
+
const SAFE = /^[A-Za-z0-9._-]$/;
|
|
39
|
+
export function encodeSegment(value) {
|
|
40
|
+
let out = '';
|
|
41
|
+
for (const char of value) {
|
|
42
|
+
if (SAFE.test(char)) {
|
|
43
|
+
out += char;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
for (const byte of new TextEncoder().encode(char)) {
|
|
47
|
+
out += `~${byte.toString(16).padStart(2, '0')}`;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return out;
|
|
51
|
+
}
|
|
52
|
+
export function decodeSegment(value) {
|
|
53
|
+
const bytes = [];
|
|
54
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
55
|
+
if (value[i] === '~' && i + 2 < value.length) {
|
|
56
|
+
bytes.push(Number.parseInt(value.slice(i + 1, i + 3), 16));
|
|
57
|
+
i += 2;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
bytes.push(...new TextEncoder().encode(value[i] ?? ''));
|
|
61
|
+
}
|
|
62
|
+
return new TextDecoder().decode(Uint8Array.from(bytes));
|
|
63
|
+
}
|
|
64
|
+
export class LayoutError extends Error {
|
|
65
|
+
constructor(message) {
|
|
66
|
+
super(message);
|
|
67
|
+
this.name = 'LayoutError';
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const ISO_DATE = /^(\d{4})-(\d{2})-(\d{2})/;
|
|
71
|
+
/**
|
|
72
|
+
* The date partition a run belongs to, taken from `startedAt`.
|
|
73
|
+
*
|
|
74
|
+
* Thrown rather than defaulted. A record whose timestamp cannot be read has no
|
|
75
|
+
* place in a time-windowed store: filed under today it would look current
|
|
76
|
+
* forever and never prune, and filed under the epoch it would vanish from
|
|
77
|
+
* every window. `ingestDirectory` catches this and reports the file as
|
|
78
|
+
* skipped, which is the visible failure the silent ones are worth trading for.
|
|
79
|
+
*/
|
|
80
|
+
export function partitionOf(startedAt) {
|
|
81
|
+
const match = ISO_DATE.exec(startedAt ?? '');
|
|
82
|
+
if (match === null) {
|
|
83
|
+
throw new LayoutError(`startedAt "${String(startedAt)}" is not an ISO 8601 timestamp, so the run has no date partition.`);
|
|
84
|
+
}
|
|
85
|
+
return `${match[1]}/${match[2]}/${match[3]}`;
|
|
86
|
+
}
|
|
87
|
+
export function runBlobName(prefix, startedAt, runId, shardKey) {
|
|
88
|
+
return `${prefix}${RUNS_ROOT}/${partitionOf(startedAt)}/${encodeSegment(runId)}/${encodeSegment(shardKey)}${SUFFIX}`;
|
|
89
|
+
}
|
|
90
|
+
/** Everything a listing needs to answer, so the common queries need no downloads. */
|
|
91
|
+
export function parseRunBlobName(prefix, name) {
|
|
92
|
+
if (!name.startsWith(`${prefix}${RUNS_ROOT}/`) || !name.endsWith(SUFFIX))
|
|
93
|
+
return null;
|
|
94
|
+
const rest = name.slice(`${prefix}${RUNS_ROOT}/`.length, -SUFFIX.length);
|
|
95
|
+
const parts = rest.split('/');
|
|
96
|
+
// YYYY / MM / DD / runId / shardKey. Anything else is not ours — a stray
|
|
97
|
+
// upload or a future layout, and either way not something to guess at.
|
|
98
|
+
if (parts.length !== 5)
|
|
99
|
+
return null;
|
|
100
|
+
const [year, month, day, runId, shardKey] = parts;
|
|
101
|
+
if (year === undefined || month === undefined || day === undefined)
|
|
102
|
+
return null;
|
|
103
|
+
if (runId === undefined || shardKey === undefined)
|
|
104
|
+
return null;
|
|
105
|
+
return {
|
|
106
|
+
runId: decodeSegment(runId),
|
|
107
|
+
shardKey: decodeSegment(shardKey),
|
|
108
|
+
date: `${year}-${month}-${day}`,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
/** Prefix every run blob shares, so a listing can skip anything else in the container. */
|
|
112
|
+
export function runsPrefix(prefix) {
|
|
113
|
+
return `${prefix}${RUNS_ROOT}/`;
|
|
114
|
+
}
|
|
115
|
+
//# sourceMappingURL=layout.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"layout.js","sourceRoot":"","sources":["../src/layout.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@aplaytest/store-azure",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Azure Blob Storage history store for atest \u2014 an append-only log of run records, so flake statistics outlive the run that produced them",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsc --build",
|
|
20
|
+
"test": "vitest run"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@aplaytest/core": "^0.1.0",
|
|
24
|
+
"@azure/core-auth": "^1.11.0",
|
|
25
|
+
"@azure/identity": "^4.13.0",
|
|
26
|
+
"@azure/storage-blob": "^12.30.0"
|
|
27
|
+
},
|
|
28
|
+
"publishConfig": {
|
|
29
|
+
"access": "public"
|
|
30
|
+
},
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "git+https://github.com/ianoflynnautomation/aplaytest.git",
|
|
34
|
+
"directory": "packages/store-azure"
|
|
35
|
+
},
|
|
36
|
+
"homepage": "https://github.com/ianoflynnautomation/aplaytest#readme",
|
|
37
|
+
"bugs": {
|
|
38
|
+
"url": "https://github.com/ianoflynnautomation/aplaytest/issues"
|
|
39
|
+
}
|
|
40
|
+
}
|