karfau/debug

References

Referenced 3 times

Testing is work in progress, this still has bugs!

Readme
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
import { sleep } from "https://esm.town/v/stevekrouse/sleep?v=1";
import { debug } from "https://esm.town/v/karfau/debug";
import { searchParams } from "https://esm.town/v/stevekrouse/searchParams?v=9";
import { mainReference } from "https://esm.town/v/karfau/mainReference";
/**
* Tries to acquire a distributed lock for `ttl` seconds.
* - the `id` defaults to `val_town-userHandle-valName` of the calling val
* - (currently) rejects ids longer then 128 chars
* - the `ttl` defauls to 3 sec
* - rejects ttl that are not safe integers or are <= 0
* @see https://dlock.univalent.net/
* be aware that the `deadline` is in seconds, not the usual millisecond
* and that it is floored, so it is very liely up to a second shorter then the ttl
* @see https://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html
* Forked from @stevekrouse.dlock
*/
export async function dlock({ id, ttl = 3 }: {
// lock id by default it is based on the name of the parent
id?: string;
// seconds
ttl?: number;
} = {}, _fetch = fetch): Promise<Lock> {
if (!id) {
id = mainReference(({ userHandle, valName }) =>
`val_town-${userHandle}-${valName}`
);
}
// https://github.com/losfair/dlock/blob/5f042396e4a42b7fcd384d188c9a3a81e973eaaa/src/index.ts#L19
if (id.length > 128) {
return Promise.reject(
new RangeError(
`id needs to be max 128 characters but was ${id.length}(${id})`,
),
);
// IDEA: (always) do a SHA512 hash from the composed or passed id, to avoid this problem
// https://examples.deno.land/hashing (needs imports from deno.land to provide types)
}
// https:github.com/losfair/dlock/blob/5f042396e4a42b7fcd384d188c9a3a81e973eaaa/src/index.ts#L53
if (!Number.isSafeInteger(ttl) || ttl <= 0) {
throw new RangeError("ttl has to be a safe integer above 0 but was " + ttl);
}
const throttleMs = ttl * 251;
let _lease = undefined;
let _deadline;
// https://github.com/losfair/dlock/blob/5f042396e4a42b7fcd384d188c9a3a81e973eaaa/src/index.ts#L48
const isExpired = (): boolean => _deadline < (Date.now() / 1000);
const release = async () =>
!isExpired() &&
void _fetch(
`https://dlock.univalent.net/lock/${id}/release?${
searchParams({ lease: _lease })
}`,
).catch(console.error);
const refresh = async () => {
if (isExpired()) {
throw new Error("lock is expired");
}
return _fetch(
`https://dlock.univalent.net/lock/${id}/aquire?${
searchParams({ lease: _lease, ttl })
}`,
).then(async (res) => {
const data = await res.json();
if (!res.ok) {
throw new Error(data.error || res.status + " " + res.statusText);
}
_deadline = data.deadline;
return _deadline;
});
};
const acquire = async (max = Date.now() + ttl * 1000): Promise<Lock> => {
try {
const lockRes = await _fetch(
`https://dlock.univalent.net/lock/${id}/aquire?${
searchParams({ lease: _lease, ttl })
}`,
);
const { lease, error, deadline } = debug(
await lockRes.json(),
id,
);
if (error && deadline) {
const deadlineMs = deadline * 1000;
if (deadlineMs >= max) {
throw new Error(error + " (and deadline > max)");
}
else {
let waitTime = deadlineMs - Date.now() + 1;
if (waitTime <= 0) {
waitTime = throttleMs;
}
await sleep(waitTime);
return acquire(max);
}
}
else if (!lockRes.ok || error) {
throw new Error(error || lockRes.status + " " + lockRes.statusText);
}
else if (lease && deadline) {

Test runner

to be able to run a number of tests (e.g. on a different val). check the references for seeing how it is used. It is extracted into a val to avoid having all that clutter in the same val as your tests.

Each test is a named function (which can be async), the name is used as the name for the test.

  • the input passed as the first argument is passed to each test, great for importing assertion methods, stubs, fixed values, ... everything that you do not mutate during a test
  • if a function is async (it returns a promise) there is a timeout of 2 seconds before the test is marked as failed.
  • all tests are called in the declared order, but async tests run in parallel afterwards, so don't assume any order
  • if a test starts with skip it is not executed
  • if a test fails it throws the output, so it appears in the read box below the val and the evaluation/run is marked red
  • if all tests pass it returns the output, so it appears in the grey box and the evaluation/run is marked green.

Note: If you are using the test runner to store the result in that val, as described above, it is considered a "JSON val" and has a run button, but it also means that another of your vals could update the val with just any other (JSON) state. Alternatively you can define a function val that calls the test runner and have a separete val to keep the curretn test results, but it means after updating the tests you need to fest save that val and then reevaluate to val storing the test state.

Readme
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
import { getRaw } from "https://esm.town/v/karfau/getRaw";
import { sleep } from "https://esm.town/v/stevekrouse/sleep?v=1";
export async function testRunner<
Input extends {
val?: Ref;
},
>(input: Input, ...tests: Test<Input>[]) {
let reportHeader = "";
try {
const [userHandle, valName] = input.val || [];
if (userHandle && valName) {
const version = await getRaw([
userHandle,
valName,
], "version");
if (typeof version === "number") {
reportHeader = `Testing @${userHandle}.${valName} v${version} ${new Date().toISOString()}:\n`;
}
}
}
catch {
}
let failedCount = 0;
let skipCount = 0;
let start = Date.now();
const result = (await Promise.all(tests.map(async (t, index) => {
const name = t.name.replaceAll(/_/g, " ");
if (name.startsWith("skip")) {
skipCount++;
return `${index + 1} [${name}]`;
}
const tstart = Date.now();
try {
const p = t(input);
if (p && p instanceof Promise) {
await Promise.race([
p,
sleep(2000).then(() => {
throw "async test did not complete in time";
}),
]);
}
return `${index + 1} ${name}: passed in ${Date.now() - tstart}ms`;
}
catch (error) {
failedCount++;
return `${index + 1} ${name}: failed after ${Date.now() - tstart}ms\n ${
/^assert/i.test(error.toString()) ? error : error.stack
}`;
}
}))).join("\n").replaceAll(/\033\[[\d;]+m/g, "");
if (failedCount) {
throw `${reportHeader}${failedCount} of ${tests.length} tests failed ${
skipCount ? `, ${skipCount} skipped` : ""
} (${Date.now() - start}ms):\n${result}`;
}
return `${reportHeader}${tests.length - skipCount} tests passed ${skipCount ? `, ${skipCount} skipped` : ""} (${
Date.now() - start
}ms):\n${result}`;
}
type Ref = [
userHandle: string,
valName: string,
];
type Test<
Input extends {
val?: Ref;
},
> = ((input: Input) => void) | ((input: Input) => Promise<void>);

Finds the top level named val that triggered the execution of this val. (Ignores any untitled vals, since those are sometimes created as an execution context.)

By passing a function as the first argument you receive all references as arguments, and can pick or transform them, e.g.

Possible Limitation: https://discord.com/channels/1020432421243592714/1143386413748994143

Readme
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
export function mainReference<T = ValRef>(transform?: Transform<T>): T {
const stack = new Error().stack;
let refs: ValRef[] = stack.match(/@@({.+?})@@/g).reverse().map((e) =>
JSON.parse(e.slice(2, -2))
);
const splitStack = stack.split("\n ");
//console.log("mainReference:stack", splitStack.length, splitStack);
//console.log("mainReference:all", refs);
refs = refs.filter(({ userHandle, valName, callNumber }) => {
if (
typeof userHandle !== "string" || typeof valName !== "string" ||
typeof callNumber !== "number"
)
return false;
// sometimes a dynamic val is created on the top level
if (valName.startsWith("untitled_"))
return false;
return true;
});
// console.log('mainReference:filtered', refs);
if (transform)
return transform(...refs);
return refs[0] as T;
}
type ValRef = {
userHandle: string;
valName: string;
callNumber?: number;
};
type Transform<T> = (...ref: ValRef[]) => T;
1
Next