@shipload/sdk 1.0.0-next.61 → 1.0.0-next.62
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/lib/shipload.d.ts +72 -31
- package/lib/shipload.js +110 -43
- package/lib/shipload.js.map +1 -1
- package/lib/shipload.m.js +108 -44
- package/lib/shipload.m.js.map +1 -1
- package/package.json +1 -1
- package/src/index-module.ts +10 -2
- package/src/managers/context.ts +9 -0
- package/src/managers/index.ts +1 -0
- package/src/managers/jobs.test.ts +62 -0
- package/src/managers/jobs.ts +52 -0
- package/src/scheduling/jobs.test.ts +32 -0
- package/src/scheduling/jobs.ts +37 -0
- package/src/shipload.ts +5 -0
package/package.json
CHANGED
package/src/index-module.ts
CHANGED
|
@@ -51,6 +51,7 @@ export {
|
|
|
51
51
|
computeFreeCells,
|
|
52
52
|
NftManager,
|
|
53
53
|
ConstructionManager,
|
|
54
|
+
JobsManager,
|
|
54
55
|
} from './managers'
|
|
55
56
|
export type {GridCell, ClusterCell, Cluster} from './managers'
|
|
56
57
|
export type {
|
|
@@ -288,8 +289,15 @@ export type {
|
|
|
288
289
|
export {taskCargoChanges} from './scheduling/task-cargo'
|
|
289
290
|
export type {TaskCargoChange, TaskCargoDirection} from './scheduling/task-cargo'
|
|
290
291
|
|
|
291
|
-
export {
|
|
292
|
-
|
|
292
|
+
export {
|
|
293
|
+
jobsToLanes,
|
|
294
|
+
pickFabricator,
|
|
295
|
+
socketTail,
|
|
296
|
+
JOB_QUEUE_CAP,
|
|
297
|
+
jobStatus,
|
|
298
|
+
splitJobCargo,
|
|
299
|
+
} from './scheduling/jobs'
|
|
300
|
+
export type {JobWindow, JobLane, JobLaneEntry, JobStatus, OwnedJob} from './scheduling/jobs'
|
|
293
301
|
|
|
294
302
|
export {composeIdleResolve} from './scheduling/idle-resolve'
|
|
295
303
|
export type {CounterpartLookup, IdleResolveTarget} from './scheduling/idle-resolve'
|
package/src/managers/context.ts
CHANGED
|
@@ -11,6 +11,7 @@ import {EpochsManager} from './epochs'
|
|
|
11
11
|
import {ActionsManager} from './actions'
|
|
12
12
|
import {ClusterManager} from './cluster'
|
|
13
13
|
import {NftManager} from './nft'
|
|
14
|
+
import {JobsManager} from './jobs'
|
|
14
15
|
import {SubscriptionsManager} from '../subscriptions/manager'
|
|
15
16
|
|
|
16
17
|
export class GameContext {
|
|
@@ -22,6 +23,7 @@ export class GameContext {
|
|
|
22
23
|
private _actions?: ActionsManager
|
|
23
24
|
private _clusters?: ClusterManager
|
|
24
25
|
private _nft?: NftManager
|
|
26
|
+
private _jobs?: JobsManager
|
|
25
27
|
private _subscriptions?: SubscriptionsManager
|
|
26
28
|
private _subscriptionsUrl?: string
|
|
27
29
|
|
|
@@ -91,6 +93,13 @@ export class GameContext {
|
|
|
91
93
|
return this._nft
|
|
92
94
|
}
|
|
93
95
|
|
|
96
|
+
get jobs(): JobsManager {
|
|
97
|
+
if (!this._jobs) {
|
|
98
|
+
this._jobs = new JobsManager(this)
|
|
99
|
+
}
|
|
100
|
+
return this._jobs
|
|
101
|
+
}
|
|
102
|
+
|
|
94
103
|
setSubscriptionsUrl(url: string) {
|
|
95
104
|
this._subscriptionsUrl = url
|
|
96
105
|
}
|
package/src/managers/index.ts
CHANGED
|
@@ -12,6 +12,7 @@ export {ClusterManager, computeFreeCells} from './cluster'
|
|
|
12
12
|
export type {GridCell, ClusterCell, Cluster} from './cluster'
|
|
13
13
|
export {NftManager} from './nft'
|
|
14
14
|
export type {NftConfigForItem} from './nft'
|
|
15
|
+
export {JobsManager} from './jobs'
|
|
15
16
|
export {PlotManager} from './plot'
|
|
16
17
|
export type {PlotProgress, PlotProgressInputRow} from './plot'
|
|
17
18
|
export {ConstructionManager} from './construction'
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import {describe, expect, it} from 'bun:test'
|
|
2
|
+
import {Name} from '@wharfkit/antelope'
|
|
3
|
+
import {JobsManager} from './jobs'
|
|
4
|
+
|
|
5
|
+
const OWNER = 'eggmaple.gm'
|
|
6
|
+
const row = (over: Record<string, unknown> = {}) => ({
|
|
7
|
+
id: {toNumber: () => 7},
|
|
8
|
+
workshop: {toNumber: () => 42},
|
|
9
|
+
socket: {toNumber: () => 0},
|
|
10
|
+
owner: Name.from(OWNER),
|
|
11
|
+
ship_id: {toNumber: () => 100},
|
|
12
|
+
coords: {x: {toNumber: () => 12}, y: {toNumber: () => 34}},
|
|
13
|
+
starts_at: {toDate: () => new Date('2026-07-26T10:00:00Z')},
|
|
14
|
+
completes_at: {toDate: () => new Date('2026-07-26T11:00:00Z')},
|
|
15
|
+
recipe_id: {toNumber: () => 10001},
|
|
16
|
+
quantity: {toNumber: () => 5},
|
|
17
|
+
energy_paid: {toNumber: () => 0},
|
|
18
|
+
cargo: [{item: 'in'}, {item: 'out'}],
|
|
19
|
+
...over,
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
function managerWith(queryImpl: () => Promise<unknown[]>, allImpl: () => Promise<unknown[]>) {
|
|
23
|
+
const table = () => ({query: () => ({all: queryImpl}), all: allImpl})
|
|
24
|
+
const ctx = {server: {table}} as never
|
|
25
|
+
return new JobsManager(ctx)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe('JobsManager.getOwnedJobs', () => {
|
|
29
|
+
it('parses owner rows: output = last cargo, inputs = rest, status derived', async () => {
|
|
30
|
+
const m = managerWith(
|
|
31
|
+
async () => [row()],
|
|
32
|
+
async () => []
|
|
33
|
+
)
|
|
34
|
+
const jobs = await m.getOwnedJobs(OWNER, {now: new Date('2026-07-26T11:30:00Z')})
|
|
35
|
+
expect(jobs).toHaveLength(1)
|
|
36
|
+
expect(jobs[0]).toMatchObject({id: 7, workshop: 42, quantity: 5, status: 'ready'})
|
|
37
|
+
expect(jobs[0].coords).toEqual({x: 12, y: 34})
|
|
38
|
+
expect(jobs[0].output).toEqual({item: 'out'} as never)
|
|
39
|
+
expect(jobs[0].inputs).toEqual([{item: 'in'}] as never)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('drops rows whose owner does not match (positional-index safety re-filter)', async () => {
|
|
43
|
+
const other = row({owner: Name.from('someoneelse.gm')})
|
|
44
|
+
const m = managerWith(
|
|
45
|
+
async () => [row(), other],
|
|
46
|
+
async () => []
|
|
47
|
+
)
|
|
48
|
+
const jobs = await m.getOwnedJobs(OWNER)
|
|
49
|
+
expect(jobs).toHaveLength(1)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('falls back to a full-scan .all() when the positional query throws', async () => {
|
|
53
|
+
const m = managerWith(
|
|
54
|
+
async () => {
|
|
55
|
+
throw new Error('bad index_position')
|
|
56
|
+
},
|
|
57
|
+
async () => [row()]
|
|
58
|
+
)
|
|
59
|
+
const jobs = await m.getOwnedJobs(OWNER)
|
|
60
|
+
expect(jobs).toHaveLength(1)
|
|
61
|
+
})
|
|
62
|
+
})
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import {Name, type NameType} from '@wharfkit/antelope'
|
|
2
|
+
import {BaseManager} from './base'
|
|
3
|
+
import {jobStatus, splitJobCargo, type OwnedJob} from '../scheduling/jobs'
|
|
4
|
+
import type {ServerContract} from '../contracts'
|
|
5
|
+
|
|
6
|
+
type JobRow = ServerContract.Types.job_row
|
|
7
|
+
|
|
8
|
+
export class JobsManager extends BaseManager {
|
|
9
|
+
async getOwnedJobs(owner: NameType, opts?: {now?: Date}): Promise<OwnedJob[]> {
|
|
10
|
+
const ownerName = Name.from(owner)
|
|
11
|
+
const now = opts?.now ?? new Date()
|
|
12
|
+
|
|
13
|
+
let rows: JobRow[]
|
|
14
|
+
try {
|
|
15
|
+
// index_position 'tertiary' = nodeos slot 3 = owner's secondary index; no ABI metadata for it.
|
|
16
|
+
rows = (await this.server
|
|
17
|
+
.table('jobs')
|
|
18
|
+
.query({
|
|
19
|
+
index_position: 'tertiary',
|
|
20
|
+
key_type: 'i64',
|
|
21
|
+
from: ownerName.value,
|
|
22
|
+
to: ownerName.value,
|
|
23
|
+
})
|
|
24
|
+
.all()) as JobRow[]
|
|
25
|
+
} catch {
|
|
26
|
+
rows = (await this.server.table('jobs').all()) as JobRow[]
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return rows.filter((r) => ownerName.equals(r.owner)).map((r) => this.parseOwnedJob(r, now))
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
private parseOwnedJob(r: JobRow, now: Date): OwnedJob {
|
|
33
|
+
const startsAt = r.starts_at.toDate()
|
|
34
|
+
const completesAt = r.completes_at.toDate()
|
|
35
|
+
const {output, inputs} = splitJobCargo(r.cargo)
|
|
36
|
+
return {
|
|
37
|
+
id: r.id.toNumber(),
|
|
38
|
+
workshop: r.workshop.toNumber(),
|
|
39
|
+
socket: r.socket.toNumber(),
|
|
40
|
+
shipId: r.ship_id.toNumber(),
|
|
41
|
+
coords: {x: r.coords.x.toNumber(), y: r.coords.y.toNumber()},
|
|
42
|
+
startsAt,
|
|
43
|
+
completesAt,
|
|
44
|
+
recipeId: r.recipe_id.toNumber(),
|
|
45
|
+
quantity: r.quantity.toNumber(),
|
|
46
|
+
status: jobStatus({startsAt, completesAt}, now),
|
|
47
|
+
output,
|
|
48
|
+
inputs,
|
|
49
|
+
outputStats: output?.stats === undefined ? undefined : BigInt(output.stats.toString()),
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import {describe, expect, it} from 'bun:test'
|
|
2
|
+
import {jobStatus, splitJobCargo} from './jobs'
|
|
3
|
+
|
|
4
|
+
const at = (s: string) => new Date(s)
|
|
5
|
+
|
|
6
|
+
describe('jobStatus', () => {
|
|
7
|
+
const job = {startsAt: at('2026-07-26T10:00:00Z'), completesAt: at('2026-07-26T11:00:00Z')}
|
|
8
|
+
it('is waiting before startsAt', () => {
|
|
9
|
+
expect(jobStatus(job, at('2026-07-26T09:59:59Z'))).toBe('waiting')
|
|
10
|
+
})
|
|
11
|
+
it('is crafting between startsAt and completesAt', () => {
|
|
12
|
+
expect(jobStatus(job, at('2026-07-26T10:30:00Z'))).toBe('crafting')
|
|
13
|
+
})
|
|
14
|
+
it('is ready at or after completesAt', () => {
|
|
15
|
+
expect(jobStatus(job, at('2026-07-26T11:00:00Z'))).toBe('ready')
|
|
16
|
+
expect(jobStatus(job, at('2026-07-26T12:00:00Z'))).toBe('ready')
|
|
17
|
+
})
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
describe('splitJobCargo', () => {
|
|
21
|
+
it('takes the last element as output, the rest as inputs', () => {
|
|
22
|
+
const cargo = [{n: 'a'}, {n: 'b'}, {n: 'out'}] as unknown as Parameters<
|
|
23
|
+
typeof splitJobCargo
|
|
24
|
+
>[0]
|
|
25
|
+
const {output, inputs} = splitJobCargo(cargo)
|
|
26
|
+
expect(output).toEqual({n: 'out'} as never)
|
|
27
|
+
expect(inputs).toEqual([{n: 'a'}, {n: 'b'}] as never)
|
|
28
|
+
})
|
|
29
|
+
it('returns null output for empty cargo', () => {
|
|
30
|
+
expect(splitJobCargo([])).toEqual({output: null, inputs: []})
|
|
31
|
+
})
|
|
32
|
+
})
|
package/src/scheduling/jobs.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import type {ServerContract} from '../contracts'
|
|
2
|
+
|
|
3
|
+
type CargoItem = ServerContract.Types.cargo_item
|
|
4
|
+
|
|
1
5
|
export interface JobWindow {
|
|
2
6
|
id: number
|
|
3
7
|
socket: number
|
|
@@ -6,6 +10,8 @@ export interface JobWindow {
|
|
|
6
10
|
completesAt: Date
|
|
7
11
|
recipeId: number
|
|
8
12
|
quantity: number
|
|
13
|
+
/** Packed stat roll of the job's output, when the source carried the job's cargo. */
|
|
14
|
+
outputStats?: bigint
|
|
9
15
|
}
|
|
10
16
|
|
|
11
17
|
export interface JobLaneEntry {
|
|
@@ -69,3 +75,34 @@ export function pickFabricator(
|
|
|
69
75
|
}
|
|
70
76
|
return best
|
|
71
77
|
}
|
|
78
|
+
|
|
79
|
+
export type JobStatus = 'waiting' | 'crafting' | 'ready'
|
|
80
|
+
|
|
81
|
+
export function jobStatus(job: {startsAt: Date; completesAt: Date}, now: Date): JobStatus {
|
|
82
|
+
if (now < job.startsAt) return 'waiting'
|
|
83
|
+
if (now < job.completesAt) return 'crafting'
|
|
84
|
+
return 'ready'
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Generic in the element so raw chain JSON (readonly actions) splits by the same rule as decoded rows.
|
|
88
|
+
export function splitJobCargo<T>(cargo: readonly T[]): {output: T | null; inputs: T[]} {
|
|
89
|
+
if (cargo.length === 0) return {output: null, inputs: []}
|
|
90
|
+
return {output: cargo[cargo.length - 1], inputs: cargo.slice(0, -1)}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface OwnedJob {
|
|
94
|
+
id: number
|
|
95
|
+
workshop: number
|
|
96
|
+
socket: number
|
|
97
|
+
shipId: number
|
|
98
|
+
coords: {x: number; y: number}
|
|
99
|
+
startsAt: Date
|
|
100
|
+
completesAt: Date
|
|
101
|
+
recipeId: number
|
|
102
|
+
quantity: number
|
|
103
|
+
status: JobStatus
|
|
104
|
+
output: CargoItem | null
|
|
105
|
+
inputs: CargoItem[]
|
|
106
|
+
/** Packed stat roll of `output`, so every job shape answers this the same way. */
|
|
107
|
+
outputStats?: bigint
|
|
108
|
+
}
|
package/src/shipload.ts
CHANGED
|
@@ -12,6 +12,7 @@ import type {EpochsManager} from './managers/epochs'
|
|
|
12
12
|
import type {ActionsManager} from './managers/actions'
|
|
13
13
|
import type {ClusterManager} from './managers/cluster'
|
|
14
14
|
import type {NftManager} from './managers/nft'
|
|
15
|
+
import type {JobsManager} from './managers/jobs'
|
|
15
16
|
import type {SubscriptionsManager} from './subscriptions/manager'
|
|
16
17
|
import type {GameState} from './entities/gamestate'
|
|
17
18
|
|
|
@@ -132,6 +133,10 @@ export class Shipload {
|
|
|
132
133
|
return this._context.nft
|
|
133
134
|
}
|
|
134
135
|
|
|
136
|
+
get jobs(): JobsManager {
|
|
137
|
+
return this._context.jobs
|
|
138
|
+
}
|
|
139
|
+
|
|
135
140
|
get subscriptions(): SubscriptionsManager {
|
|
136
141
|
return this._context.subscriptions
|
|
137
142
|
}
|