Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
174 changes: 174 additions & 0 deletions api_tests/api_test_lock_caller_caller/client/app.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
/**
* @Project: @cldmv/slothlet
* @Filename: /api_tests/api_test_lock_caller_caller/client/app.mjs
* @Date: 2026-09-28 00:00:00 -07:00 (1790578800)
* @Author: Nate Corcoran <CLDMV>
* @Email: <Shinrai@users.noreply.github.com>
* -----
* @Last modified by: Nate Corcoran <CLDMV> (Shinrai@users.noreply.github.com)
* @Last modified time: 2026-09-28 00:00:00 -07:00 (1790578800)
* -----
* @Copyright: Copyright (c) 2013-2026 Catalyzed Motivation Inc. All rights reserved.
*/

/**
* @fileoverview Client module for the `lockCaller.caller()` tests (#477). Schedules jobs on the
* scheduler service; each job calls a probe target, so the target sees whichever identity the job
* runs as.
*
* @module api_tests/api_test_lock_caller_caller/client/app
*/

import { self } from "@cldmv/slothlet/runtime";

/**
* Schedule a job that reports who the target sees calling it, pinned to this client.
* @returns {number} Stored job count.
* @public
* @example
* api.client.app.scheduleWhoami();
*/
export function scheduleWhoami() {
return self.scheduler.service.every(() => self.target.probe.whoami());
}

/**
* Same job, but the scheduler pins itself (`lockCaller`), for comparison.
* @returns {number} Stored job count.
* @public
* @example
* api.client.app.scheduleWhoamiAsService();
*/
export function scheduleWhoamiAsService() {
return self.scheduler.service.everyAsSelf(() => self.target.probe.whoami());
}

/**
* Schedule a job that calls the client-only guarded route.
* @returns {number} Stored job count.
* @public
* @example
* api.client.app.scheduleGuarded();
*/
export function scheduleGuarded() {
return self.scheduler.service.every(() => self.target.probe.guarded());
}

/**
* Schedule the guarded job with the scheduler pinning itself, for comparison.
* @returns {number} Stored job count.
* @public
* @example
* api.client.app.scheduleGuardedAsService();
*/
export function scheduleGuardedAsService() {
return self.scheduler.service.everyAsSelf(() => self.target.probe.guarded());
}

/**
* Schedule an async job that awaits a timer before reporting who the target sees.
* @returns {number} Stored job count.
* @public
* @example
* api.client.app.scheduleAsyncWhoami();
*/
export function scheduleAsyncWhoami() {
return self.scheduler.service.every(async () => {
await new Promise((resolve) => setTimeout(resolve, 0));
return self.target.probe.whoami();
});
}

/**
* Schedule a job that throws a TypeError.
* @returns {number} Stored job count.
* @public
* @example
* api.client.app.scheduleThrow();
*/
export function scheduleThrow() {
return self.scheduler.service.every(() => {
throw new TypeError("job-boom");
});
}

/**
* Schedule a job that returns its own `this` and arguments.
* @returns {number} Stored job count.
* @public
* @example
* api.client.app.scheduleThisAndArgs();
*/
export function scheduleThisAndArgs() {
return self.scheduler.service.every(function thisAndArgs(...args) {
return { self: this, args };
});
}

/**
* Schedule a job that calls the principal-gated route for the project it is fired with.
* @returns {number} Stored job count.
* @public
* @example
* api.client.app.scheduleFiles();
*/
export function scheduleFiles() {
return self.scheduler.service.every((projectId) => self.target.probe.files(projectId));
}

/**
* Call the principal-gated route directly as this client.
* @param {string} projectId - Project id.
* @returns {{projectId: string}} The project marker.
* @public
* @example
* api.client.app.readFiles("p1");
*/
export function readFiles(projectId) {
return self.target.probe.files(projectId);
}

/**
* The principal-gated job with the scheduler pinning itself, for comparison.
* @returns {number} Stored job count.
* @public
* @example
* api.client.app.scheduleFilesAsService();
*/
export function scheduleFilesAsService() {
return self.scheduler.service.everyAsSelf((projectId) => self.target.probe.files(projectId));
}

/**
* Ask the scheduler for a pinned wrapper and hand back both it and the original callback.
* @returns {{wrapper: Function, original: Function}} The wrapper and the callback it pins.
* @public
* @example
* api.client.app.pinThroughScheduler();
*/
export function pinThroughScheduler() {
const original = () => "original";
return { wrapper: self.scheduler.service.pin(original), original };
}

/**
* Call `lockCaller.caller` directly from this (ungranted) client.
* @returns {Function} The pinned wrapper (only when granted).
* @public
* @example
* api.client.app.pinDirectly();
*/
export function pinDirectly() {
return self.slothlet.lockCaller.caller(() => 0);
}

/**
* Fire the scheduler from inside this client, so the client is the ambient identity.
* @returns {Promise<Array<*>>} The jobs' results.
* @public
* @example
* await api.client.app.fireFromClient();
*/
export function fireFromClient() {
return self.scheduler.service.fire();
}
51 changes: 51 additions & 0 deletions api_tests/api_test_lock_caller_caller/roles/registry.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/**
* @Project: @cldmv/slothlet
* @Filename: /api_tests/api_test_lock_caller_caller/roles/registry.mjs
* @Date: 2026-09-28 00:00:00 -07:00 (1790578800)
* @Author: Nate Corcoran <CLDMV>
* @Email: <Shinrai@users.noreply.github.com>
* -----
* @Last modified by: Nate Corcoran <CLDMV> (Shinrai@users.noreply.github.com)
* @Last modified time: 2026-09-28 00:00:00 -07:00 (1790578800)
* -----
* @Copyright: Copyright (c) 2013-2026 Catalyzed Motivation Inc. All rights reserved.
*/

/**
* @fileoverview Owner of the "roles" principal (#459) for the `lockCaller.caller()` tests (#477).
* Grants are per user, per project.
*
* @module api_tests/api_test_lock_caller_caller/roles/registry
*/

import { self } from "@cldmv/slothlet/runtime";

const grants = new Map();

/**
* Register the "roles" principal, keyed by `ctx.user`.
* @returns {void}
* @public
* @example
* api.roles.registry.setup();
*/
export function setup() {
self.slothlet.permissions.principal.register("roles", {
key: (ctx) => ctx.user,
resolve: async (user) => ({ projects: { ...(grants.get(user) ?? {}) } })
});
}

/**
* Grant a user permissions on a project.
* @param {string} user - User id.
* @param {string} projectId - Project id.
* @param {string[]} perms - Permissions.
* @returns {void}
* @public
* @example
* api.roles.registry.grant("u1", "p1", ["read"]);
*/
export function grant(user, projectId, perms) {
grants.set(user, { ...(grants.get(user) ?? {}), [projectId]: perms });
}
126 changes: 126 additions & 0 deletions api_tests/api_test_lock_caller_caller/scheduler/service.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
/**
* @Project: @cldmv/slothlet
* @Filename: /api_tests/api_test_lock_caller_caller/scheduler/service.mjs
* @Date: 2026-09-28 00:00:00 -07:00 (1790578800)
* @Author: Nate Corcoran <CLDMV>
* @Email: <Shinrai@users.noreply.github.com>
* -----
* @Last modified by: Nate Corcoran <CLDMV> (Shinrai@users.noreply.github.com)
* @Last modified time: 2026-09-28 00:00:00 -07:00 (1790578800)
* -----
* @Copyright: Copyright (c) 2013-2026 Catalyzed Motivation Inc. All rights reserved.
*/

/**
* @fileoverview Scheduler-style service for the `lockCaller.caller()` tests (#477).
*
* @description
* Accepts callbacks on behalf of whoever calls it and runs them later from {@link fire}. `every`
* pins the CALLER of `every` onto the job (`self.slothlet.lockCaller.caller`); `everyAsSelf` pins the
* service itself (`self.slothlet.lockCaller`) for comparison.
*
* @module api_tests/api_test_lock_caller_caller/scheduler/service
*/

import { self } from "@cldmv/slothlet/runtime";

let jobs = [];

/**
* Store a job pinned to the identity of whoever called this leaf.
* @param {Function} job - Callback to run on {@link fire}.
* @returns {number} The number of stored jobs.
* @public
* @example
* api.scheduler.service.every(() => 1);
*/
export function every(job) {
jobs.push(self.slothlet.lockCaller.caller(job));
return jobs.length;
}

/**
* Store a job pinned to this service (plain `lockCaller`).
* @param {Function} job - Callback to run on {@link fire}.
* @returns {number} The number of stored jobs.
* @public
* @example
* api.scheduler.service.everyAsSelf(() => 1);
*/
export function everyAsSelf(job) {
jobs.push(self.slothlet.lockCaller(job));
return jobs.length;
}

/**
* Return the `lockCaller.caller()` wrapper for `job` without storing it.
* @param {Function} job - Callback to pin.
* @returns {Function} The pinned wrapper.
* @public
* @example
* api.scheduler.service.pin(() => 1);
*/
export function pin(job) {
return self.slothlet.lockCaller.caller(job);
}

/**
* Run every stored job, sequentially, forwarding `args`.
* @param {...*} args - Arguments forwarded to each job.
* @returns {Promise<Array<*>>} Each job's (awaited) result.
* @public
* @example
* await api.scheduler.service.fire();
*/
export async function fire(...args) {
const results = [];
for (const job of jobs) results.push(await job(...args));
return results;
}

/**
* Run every stored job concurrently, forwarding `args`.
* @param {...*} args - Arguments forwarded to each job.
* @returns {Promise<Array<*>>} Each job's result.
* @public
* @example
* await api.scheduler.service.fireAll();
*/
export function fireAll(...args) {
return Promise.all(jobs.map((job) => job(...args)));
}

/**
* Run every stored job synchronously with an explicit `this`, returning the raw results.
* @param {*} thisArg - `this` binding for each job.
* @param {...*} args - Arguments forwarded to each job.
* @returns {Array<*>} Each job's result.
* @public
* @example
* api.scheduler.service.fireWith({ tag: 1 });
*/
export function fireWith(thisArg, ...args) {
return jobs.map((job) => job.apply(thisArg, args));
}

/**
* The raw stored job wrappers (for metadata inspection).
* @returns {Function[]} The stored wrappers.
* @public
* @example
* api.scheduler.service.list();
*/
export function list() {
return [...jobs];
}

/**
* Drop every stored job.
* @returns {void}
* @public
* @example
* api.scheduler.service.clear();
*/
export function clear() {
jobs = [];
}
Loading
Loading